Scalaを触ってみた~【オブジェクト】~


Introduction

dwangoのチュートリアルをそのまま移行してきたもので、自分が勉強していくついでに編集して自分の用語に置き換えたりしていきます。

オブジェクト

Scalaでは、全ての値がオブジェクトです。また、全てのメソッドは何らかのオブジェクトに所属しています。そのため、Javaのようにクラスに属するstaticフィールドやstaticメソッドといったものを作成することができません。その代わりに、objectキーワードによって、同じ名前のシングルトンオブジェクトを現在の名前空間の下に1つ定義することができます。objectキーワードによって定義したシングルトンオブジェクトには、そのオブジェクト固有のメソッドやフィールドを定義することができます。

object構文の主な用途としては、

ユーティリティメソッドやグローバルな状態の置き場所(Javaで言うstaticメソッドやフィールド)
同名クラスのオブジェクトのファクトリメソッド
が挙げられます。

objectの基本構文はクラスとおおむね同じで、

object <オブジェクト名> extends <クラス名> (with <トレイト名>)* {
  (<フィールド定義> | <メソッド定義>)*
}

となります。Scalaでは標準でPredefというobjectが定義・インポートされており、これは最初の使い方に当てはまります。 println("Hello") となにげなく使っていたメソッドも実はPredef のメソッドなのです。 extends でクラスを継承、 with でトレイトをmix-in 可能になっているのは、オブジェクト名を既存のクラスのサブクラス等として振る舞わせたい場合があるからです。Scala の標準ライブラリでは、 Nil という object がありますが、これは List の一種として振る舞わせたいため、 List を継承しています。一方、 object がトレイトをmix-inする事はあまり多くありませんが、クラスやトレイトとの構文の互換性のためにそうなっていると思われます。

一方、2番めの使い方について考えてみます。点を表す Point クラスのファクトリを objectで作ろうとすると、次のようになります。apply という名前のメソッドはScala処理系によって特別に扱われ、Point(x)のような記述があった場合で、Point objectにapplyという名前のメソッドが定義されていた場合、Point.apply(x)と解釈されます。これを利用してPoint objectの applyメソッドでオブジェクトを生成するようにすることで、Point(3, 5)のような記述でオブジェクトを生成できるようになります。


scala> class Point(val x:Int, val y:Int)
defined class Point

scala> object Point {
     |   def apply(x: Int, y: Int): Point = new Point(x, y)
     | }
defined object Point
warning: previously defined class Point is not a companion to object Point.
Companions must be defined together; you may wish to use :paste mode for this.

これは、new Point()で直接Pointオブジェクトを生成するのに比べて、

クラス(Point)の実装詳細を内部に隠しておける(インタフェースのみを外部に公開する)
Pointではなく、そのサブクラスのインスタンスを返すことができる
といったメリットがあります。なお、上記の記述はケースクラスを用いてもっと簡単に


scala> case class Point(x: Int, y: Int)
defined class Point

と書けます。ケースクラスは後述するパターンマッチのところでも出てきますが、ここではその使い方については触れません。簡単に言うとケースクラスは、それをつけたクラスのプライマリコンストラクタ全てのフィールドを公開し、equals()・hashCode()・toString()などのオブジェクトの基本的なメソッドをオーバーライドしたクラスを生成し、また、そのクラスのインスタンスを生成するためのファクトリメソッドを生成するものです。たとえば、 case class Point(x: Int, y: Int)で定義した Point クラスは equals() メソッドを明示的に定義してはいませんが、


Point(1, 2).equals(Point(1, 2))

を評価した値はtrueになります。

コンパニオンオブジェクト
クラスと同じファイル内、同じ名前で定義されたシングルトンオブジェクトは、コンパニオンオブジェクトと呼ばれます。コンパニオンオブジェクトは対応するクラスに対して特権的なアクセス権を持っています。たとえば、 weightをprivateにした場合、


class Person(name: String, age: Int, private val weight: Int)

object Hoge {
  def printWeight(): Unit = {
    val taro = new Person("Taro", 20, 70)
    println(taro.weight)
  }
}

はNGですが、


class Person(name: String, age: Int, private val weight: Int)

object Person {
  def printWeight(): Unit = {
    val taro = new Person("Taro", 20, 70)
    println(taro.weight)
  }
}

はOKです。なお、コンパニオンオブジェクトでも、private[this](そのオブジェクト内からのみアクセス可能)なクラスのメンバーに対してはアクセスできません。単にprivateとした場合、コンパニオンオブジェクトからアクセスできるようになります。

上記のような、コンパニオンオブジェクトを使ったコードをREPLで試す場合は、REPLの:pasteコマンドを使って、クラスとコンパニオンオブジェクトを一緒にペーストするようにしてください。クラスとコンパニオンオブジェクトは同一ファイル中に置かれていなければならないのですが、REPLで両者を別々に入力した場合、コンパニオン関係をREPLが正しく認識できないのです。


scala> :paste
// Entering paste mode (ctrl-D to finish)

class Person(name: String, age: Int, private val weight: Int)

object Person {
  def printWeight(): Unit = {
    val taro = new Person("Taro", 20, 70)
    println(taro.weight)
  }
}

// Exiting paste mode, now interpreting.

defined class Person
defined object Person