case classで自動生成されるコードについて解説

Scala 3.3.1
最終更新:2023年12月7日

[AD] scalapediaでは記事作成ボランティアを募集しています

以下の記事ではcase classで出来ることを説明しました。

  • 基本コンストラクタの引数でメンバーを定義できる
  • toStringでインスタンスの内容がわかる
  • インスタンスを==で比較で
  • インスタンスをコピー
  • インスタンスを作成する際にnewキーワードが不要
  • パターンマッチ

この記事では、case classで上記が出来る理由を説明します。

基本コンストラクタの引数にはvalがつく

基本コンストラクタでは、以下のように引数を指定することができました。

case class Owner(name: String) case class Book(title: String, owner: Owner)

case classでない普通のクラスで上記のように定義した場合、「不変なprivateメンバー(private[this])」です。
詳細は以下の記事を参照してください。

case classの基本コンストラクタの引数には、valがつきます。
そのため、「不変なpublicメンバー」になります。

scalaコマンドを-Xprint:typerオプションを付けてREPLを起動すると、定義したcase classがどのように定義されるかがわかります。
実際にREPLを使用してcase classの基本コンストラクタの引数が実際にどのように定義されるのかを見てみます。

REPLでBookクラスの基本コンストラクタを定義すると、基本コンストラクタの引数がprivate[this] valで定義され、値を参照するためアクセッサが、defで定義されているのがわかります。

<caseaccessor> <paramaccessor> private[this] val title: String = _; <stable> <caseaccessor> <accessor> <paramaccessor> def title: String = Book.this.title; <caseaccessor> <paramaccessor> private[this] val owner: Owner = _; <stable> <caseaccessor> <accessor> <paramaccessor> def owner: Owner = Book.this.owner;

基本コンストラクタの引数は、以下のようにvarで定義することもできます。

case class Book(title: String, var owner: Owner)

REPLで定義内容を見てみます。

<caseaccessor> <paramaccessor> private[this] val title: String = _; <stable> <caseaccessor> <accessor> <paramaccessor> def title: String = Book.this.title; <caseaccessor> <paramaccessor> private[this] var owner: Owner = _; <caseaccessor> <accessor> <paramaccessor> def owner: Owner = Book.this.owner; <accessor> <paramaccessor> def owner_=(x$1: Owner): Unit = Book.this.owner = x$1;

varで定義した「owner」に対しては、値を参照するためのアクセッサと値を設定するためのアクセッサが定義されているのがわかります。
そのため、varを使用した場合は、値を設定するためのアクセッサを使用して値を変更することができます。

val book = Book("Hello World!", Owner("Taro")) book.owner = Owner("Hanako") println(book)
Book(Hello World!,Owner(Hanako))

varで定義した場合、思わぬ副作用を引き起こす場合がありますので、基本的にはvalを使用するのが良いでしょう。

人が理解できる形式の文字列を返すtoStringメソッドが生成される

case classtoStringメソッドは、以下のように人が理解できる形式の文字列を返します。

val book = Book("Hello World!", Owner("Taro")) println(book.toString())
Book(Hello World!,Owner(Taro))

なぜそのような動作をするのかを説明します。

case classを定義すると、toStringメソッドと他にいくつかのメソッドが追加されます。

まずはcase classに追加されたtoStringメソッドを見てみてみます。

REPLでOwnerクラスとBookクラスの定義を確認するとtoStringメソッドは、以下のようにScalaRunTime_toStringメソッドを呼び出しているのがわかります。
ScalaRuntimeはScalaのプログラムの実行に必要なサポートメソッドを提供するオブジェクトです。

override <synthetic> def toString(): String = scala.runtime.ScalaRunTime._toString(Book.this);

次にScalaRuntime_toStringメソッドを見てみます。
ScalaのソースコードはGithubで公開されています。
Scalaのリポジトリで、ScalaRuntimeのソースコードを確認します。

ScalaRuntime_toStringメソッドは、以下のようにcase classproductIteratorメソッドが返した値に対して、mkStringメソッドを呼び出して文字列を作成しています。

case classproductIteratorは、case classの基本コンストラクタで指定した値にアクセスするIteratorを返します。
productPrefixcase classの名前を返します。
そのためcase classtoStringメソッドは、「クラス名(パラメータの値1, パラメーターの値2, ...)」の形式の文字列を返します。

def _toString(x: Product): String = x.productIterator.mkString(x.productPrefix + "(", ",", ")")

「==」で構造を比較出来るメソッドが生成される

case classは、Scalaの基本型と同じように==で構造を比較することができました。

ここではその理由を説明します。

オブジェクトに対して==を適用すると、以下の記事で説明したようにequalsメソッドが呼び出されます。

case classを定義すると、コンストラクタの引数で指定した値をすべて比較するequalsメソッドがオーバーライドされます。
そのため、参照先が違っていてもコンストラクタの引数で指定した値がすべて同じであればtrueになります。

先ほどのREPLで定義したBookクラスの結果を見るとequalsメソッドは以下のように定義されています。

override <synthetic> def equals(x$1: Any): Boolean = Book.this.eq(x$1.asInstanceOf[Object]).||(x$1 match { case (_: Book) => true case _ => false }.&&({ <synthetic> val Book$1: Book = x$1.asInstanceOf[Book]; Book.this.title.==(Book$1.title).&&(Book.this.owner.==(Book$1.owner)).&&(Book$1.canEqual(Book.this)) }))

最初にeqメソッドで比較対象のオブジェクトの参照先が同じ場合はtrueを返します。
次にmatch式でクラスの型が同じ(Book)かを確認し、違う場合はfalseを返します。
最後に基本コンストラクタの引数で渡した値を==で比較してすべて同じであればtrue、違う場合はfalseを返します。

インスタンスをコピーするcopyメソッドが生成される

case classcopyメソッドを使用してインスタンスをコピーすることができました。

case classを定義するとインスタンスをコピーするcopyメソッドが生成されます。

先ほどのREPLで定義したBookクラスの結果を見るとcopyメソッドは以下のように定義されています。

<synthetic> def copy(title: String = title, owner: Owner = owner): Book = new Book(title, owner);

省略値はコピー元のインスタンスの値になっていますので、引数に指定した値のみ変更してコピーすることができます。

抽出子をもつコンパニオンオブジェクトが生成される

case classを定義すると、applyメソッドとunapplyメソッドをもつ「コンパニオンオブジェクト」が生成されます。

これらがどのように作用するのかを説明します。

applyメソッドは、newを指定せずにインスタンスを作成できるようにする

case classのインスタンスを作成する際にはnewキーワードは不要でした。
これはcase classを定義するとapplyメソッドが追加されるためです。

以下のようにBookのインスタンスを作成するとBookapplyメソッドが呼び出されます。

val book = Book("Hello World!", Owner("Taro"))

REPLでBookを定義した結果を見るとapplyメソッドは以下のように定義されています。

case <synthetic> def apply(title: String, owner: Owner): Book = new Book(title, owner);

applyメソッドではBookのインスタンスを作成して返しています。

unapplyメソッドは、パターンマッチを出来るようにする

case classのインスタンスにパターンマッチを適用して、基本コンストラクタの引数で指定した値を抽出したりmatch式に適用することができました。
これはcase classを定義するとunapplyメソッドが追加されるためです。

unapplyメソッドは、applyメソッドと逆の動きをします。
applyメソッドは引数からcase classのインスタンスを作成しましたが、unapplyメソッドはcase classのインスタンスから基本コンストラクタの引数で指定した値を抽出します。

REPLでBookを定義した結果を見るとunapplyメソッドは以下のように定義されています。

case <synthetic> def unapply(x$0: Book): Option[(String, Owner)] = if (x$0.==(null)) scala.None else Some.apply[(String, Owner)](scala.Tuple2.apply[String, Owner](x$0.title, x$0.owner));

インスタンスがnullの場合は、Noneを返します。
nullでない場合は、基本コンストラクタの引数で指定した値のTupleSomeでくるんで返します。

サイト内検索