ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

Scalaの勉強 - クラス

スポンサーリンク

今日はクラスの章を。

クラス

クラス定義

記法を除けばJavaのクラスと同等らしい。(Javaはよくわかってない)

構文は以下の通り。

class <クラス名> '(' (<引数名1> : <引数型1>, <引数名2>: <引数型2> ...)? ')' {
  (<フィールド定義> | <メソッド定義> )*
}

例えば点を示すクラスPointScalaでは以下のようになる。

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()メソッドがあるということか。pythonobjectみたく大本から派生してるものだと推測。

メソッド定義

すでに上記の例でメソッドを定義しているが一般形は以下のような形になる。

(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 型の xyz という名前を持った、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の章はおしまい。一通り終わってからスコープの件はもう少し突っ込んで確認してみよう。