今日はクラスの章を。
クラス
クラス定義
記法を除けばJavaのクラスと同等らしい。(Javaはよくわかってない)
構文は以下の通り。
class <クラス名> '(' (<引数名1> : <引数型1>, <引数名2>: <引数型2> ...)? ')' { (<フィールド定義> | <メソッド定義> )* }
例えば点を示すクラスPoint
はScalaでは以下のようになる。
class Point(_x: Int, _y: Int) { val x = _x val y = _y }
うん、なんとなくはわかる。上記の定義そのものがコンストラクタになっている感じか。
使うときは、こんな感じでよさそう。
object ClassPoint { class Point(_x: Int, _y: Int) { val x = _x val y = _y } def main(args: Array[String]): Unit = { val point = new Point(100, 2) println(point) println(point.x) println(point.y) } }
実行結果はエラーはなく、以下のようになった。
Main$Point@16b3fc9e 100 2
以下のようにして、コンストラクタの引数にval
/var
をつけて定義すると、その引数はそのまま公開フィールドになるとのこと。
class Point(val x: Int, val y: Int) { }
Scalaでは複数のコンストラクタを定義することは出来るが、基本的には1つのコンストラクタのみを使って複数の精製方法を定義する場合にはapply
メソッドを使うことが多い。この1つのコンストラクタはプライマリコンストラクタとして、特別に扱われる。
ちなみにプライマリコンストラクタ以外にコンストラクタを作る場合はthis
メソッドを用意する形になる(Scalaのリファレンスの5.3.1より)
A class may have additional constructors besides the primary constructor. These
are defined by constructor definitions of the form
def this(ps_1). . .(ps_n) = e.
あんまりいい例ではないけど、こんな感じ↓。
class Point3(val x: Int, val y: Int) { var z = 0 def this(x: Int, y: Int, z: Int) = { this(x, y); this.z = z } // 2つめのコンストラクタ }
プライマリコンストラクタの引数のスコープはクラス定義全体に及ぶので、以下のようにメソッド定義の中からコンストラクタ引数を参照できる。
class Point(val x: Int, val y: Int) { def +(p: Point): Point = { new Point(x + p.x, y + p.y) } override def toString(): String = "(" + x + ", " + y + ")" }
なんかいろいろまだ見たこと無いのがいっぱい。。けど推測は出来る。
def +(p: Point): Point
は演算子のオーバーロード的なやつdef toString()
はString
型への変換メソッドをオーバーライドしてる- ということはもともとのクラスには
toString()
メソッドがあるということか。pythonのobject
みたく大本から派生してるものだと推測。
- ということはもともとのクラスには
メソッド定義
すでに上記の例でメソッドを定義しているが一般形は以下のような形になる。
(private([this | <パッケージ名>])? | protected([<パッケージ名>])? def <メソッド名> '(' (<引数名> : 引数型 (, 引数名 : <引数型>)*)? ')': <返り値型> = <本体>
<本体>
の部分は通常はブロック式になる。<返り値型>
は省略可能だが、つけておくのがベターprivate
/protected
をつけない場合はpublic
扱いprivate/protected[this | パッケージ名]
のthis
やパッケージ名
でスコープを限定できるprivate
のみの場合:同一クラスからのみアクセス可能protected
ののみの場合:派生クラスからのアクセスも可能[this]
をつけた場合:同じオブジェクトからのみアクセス可能[パッケージ名]
をつけた場合:同一パッケージに所属している場合もアクセス可能
この辺のスコープの話はまたあとで確認が必要そう。。
複数の引数リストを持つメソッド
以下のように複数の引数リストを持つように定義することが可能
(private([this | <パッケージ名>])? | protected[<パッケージ名>]) def <メソッド名> '(' (<引数名> : 引数型 (, 引数名 : <引数型>)*) ?')' ( '(' (<引数名> : 引数型 (, 引数名 : <引数型>)*) ?')' )* : <返り値型> = <本体式>
資料のサンプルを動かしてみる。
object ClassAdder { class Adder { def add(x: Int)(y: Int): Int = x + y } def main(args: Array[String]): Unit = { val ad = new Adder() println(ad.add(100)(200)) } }
うーーむ、なんとも不思議な定義&呼び出しだ。とりあえず実行してみると、以下のようにエラーはなく動作する。
$ scala ./20180909_02_class_adder.scala
300
資料によれば、、
複数の引数リストを持つメソッドには、Scalaの糖衣構文と組み合わせて流暢なAPIを作ったり、後述するimplicit parameterのために必要になったり、型推論を補助するために使われたりといった用途があります。
とのことだが、このサンプルだけではいまいち便利さが理解できない。そのうちわかるのだろうか。。
とりあえず、続き。以下のようにすることで、最初の引数は適用済みの状態にして新しい関数にすることも出来る(部分適用というらしい)。
object ClassAdder { class Adder { def add(x: Int)(y: Int): Int = x + y } def main(args: Array[String]): Unit = { val ad = new Adder() val ad2 = ad.add(1) _ // addメソッドの最初の引数を適用した状態を作る println(ad2(4)) } }
なるほど、こうすることで特定のデータを適用済みの状態を作り出せるのか。match
式とかと組み合わせると面白そうな気配。
ちなみに、通常通りに1つの引数リストに複数の引数を定義しておき同様に部分適用した状態を作ることも可能。なんかad4
の定義が面倒になってるな。。なんだろう、これ。
object ClassAdder { class Adder2 { def add(x: Int, y: Int): Int = x + y } def main(args: Array[String]): Unit = { val ad3 = new Adder2() val ad4: Int => Int = ad3.add(1, _) println(ad4(3)) } }
フィールド定義
以下のように定義する
(private ([this | <パッケージ名>])? /protected ([<パッケージ名>])? (val/var) <フィールド名>: <フィールド型> = <初期化式>
val
/var
: 他のケースと同様に変更可能かどうかprivate
/protected
:メソッドの定義と同様private[this]
をつけるとJVMレベルでのフィールドへの直接アクセスになるので若干高速になるらしい。
抽象メンバー
抽象メンバーも定義できる。
まずはメソッドから。要は本体を書かなければいいみたい。
(private([this | <パッケージ名>])? | protected([<パッケージ名>])? def <メソッド名> '(' (<引数名> : 引数型 (, 引数名 : <引数型>)*)? ')': <返り値型>
次にフィールド。こちらもメソッドと感覚的には一緒で、初期化をしなければいい。
(private ([this | <パッケージ名>])? /protected ([<パッケージ名>])? (val/var) <フィールド名>: <フィールド型>
なお、抽象メンバーを持ったクラスを定義する際には抽象クラスとして宣言するためにabstract
修飾子が必要になる。
ということでまとめると、以下のようになるはず。
abstract class <クラス名> '(' (<引数名1> : <引数型1>, <引数名2>: <引数型2> ...)? ')' { (private ([this | <パッケージ名>])? /protected ([<パッケージ名>])? (val/var) <フィールド名>: <フィールド型>* // 抽象フィールド (private([this | <パッケージ名>])? | protected([<パッケージ名>])? def <メソッド名> '(' (<引数名> : 引数型 (, 引数名 : <引数型>)*)? ')': <返り値型>* // 抽象メソッド }
前項で出てきたAdder
を抽象クラスにしてみる。
object ClassAbstract { abstract class BaseAdder { var x:Int var y:Int def add(x: Int)(y: Int): Int } def main(args: Array[String]): Unit = { val ad = new BaseAdder() } }
抽象クラスなのでインスタンス使用しようとするとエラーになる。この辺はC++とかと一緒。
$ scala ./20180909_03_class_abstract.scala ./20180909_03_class_abstract.scala:10: error: class BaseAdder is abstract; cannot be instantiated val ad = new BaseAdder()
継承
前項で抽象クラスが出てきたが、他の限度と同様にインスタンス出来ないという挙動だった。ということで次は継承についてが出てくる。
継承の目的は以下の2つ
継承は以下のような構文。
class <クラス名> <クラス引数> (extends <スーパークラス>)? (with <トレイト名>)* { (<フィールド定義> | <メソッド定義>)* }
extends
が継承のキーワード。System Verilogと一緒か。その後に付いているトレイトを使うことで複数の実装を継承することが可能とのことだが、こちらはドワンゴの資料で別で1章解説があるのでその時に確認することにする。
先ほど作成した抽象クラスBaseAdder
を継承してインスタンス可能なクラスAdder
を作る。
object ClassAbstract { abstract class BaseAdder { var x:Int var y:Int def add(x: Int)(y: Int): Int } class SubAdder extends(BaseAdder) { var x:Int = 0 var y:Int = 0 override def add(x: Int)(y: Int): Int = x + y } def main(args: Array[String]): Unit = { val ad = new SubAdder() println(ad.add(100)(200)) } }
上記のように継承して作成したSubAdder
はインスタンス可能になり、以下のように加算された結果が得られる。
$ scala ./20180909_03_class_abstract.scala
300
次は抽象クラスではないクラスを継承してメソッドをオーバーライドしてみる。
object ClassDerived { class BaseAdder { var x:Int = 0 var y:Int = 0 def add(x: Int)(y: Int): Int = x + y } class WrongAdder extends(BaseAdder) { x = 50 override def add(x: Int)(y: Int): Int = x + y + this.x } def main(args: Array[String]): Unit = { val ad = new WrongAdder() println(ad.add(100)(200)) } }
上記を実行してみると、WrongAdder
の方ではthis.x
に50を入れており、add
メソッドでthis.x
が加算される仕組みになっているため値が350になって返ってくる。
$ scala ./20180909_04_class_derived.scala
350
なお先の抽象クラスBaseAdder
の例とは異なり、今回のBaseAdder
クラスでは既にadd
メソッドが実装されているため、SubAdder
クラスのadd
関数の宣言に付いているoverride
を外すとエラーになる。
$ scala ./20180909_04_class_derived.scala ./20180909_04_class_derived.scala:11: error: overriding method add in class BaseAdder of type (x: Int)(y: Int)Int; method add needs `override' modifier def add(x: Int)(y: Int): Int = x + y + this.x ^ one error found
練習問題
全てが
Int
型のx
、y
、z
という名前を持った、3次元座標を表すPoint3D
クラスを定義してください。Point3D
クラスは次のようにして使うことができなければいけません。
scala val p = new Point3D(10, 20, 30) println(p.x) // 10 println(p.y) // 20 println(p.z) // 30
出力は以下。解答
object ClassPractice {
class Point3D(val x: Int, val y:Int, val z:Int)
def main(args: Array[String]): Unit = {
val p = new Point3D(10, 20, 30)
println(p.x) // 10
println(p.y) // 20
println(p.z) // 30
}
}
$ scala ./20180909_05_class_practice.scala
10
20
30
これでClassの章はおしまい。一通り終わってからスコープの件はもう少し突っ込んで確認してみよう。