この記事ではScala 2.12.10を使っています。
以下のようにトレイトにフィールドを定義して、継承先でフィールドの値を固定しつつ、ファクトリメソッドでは任意の値を指定できるようにしたい時がありました。この時継承先の case class Bar()
を final
にするとコンパイルが通りません。もちろん final
を外せば問題ないのですが今度はwartremoverが通りません。
trait Version { val version: Int }
final case class Bar() extends Version { override val version = 1 } // finalを使う
object Bar {
def apply(v: Int): Bar = new Bar() { override val version = v }
}
// cmd6.sc:4: illegal inheritance from final class Bar
// def apply(v: Int): Bar = new Bar() { override val version = v }
// ^
// Compilation Failed
そこでこのように final
から sealed
に変えるとコンパイルもwartremoverも通ります。1
trait Version { val version: Int }
sealed case class Bar() extends Version { override val version = 1 } // sealedに変える
object Bar {
def apply(v: Int): Bar = new Bar() { override val version = v }
}
// defined trait Version
// defined class Bar
// defined object Bar
なぜ final
ではうまくいかないのか
Scalaには final
指定子をクラスに適用するとサブクラス化が禁止されるのに対して、 sealed
は 同じファイルで定義されているサブクラス以外 は新しいサブクラスを追加できないというルールがあります。2
今回前者の final
を使ったパターンでは一見サブクラス化はしていないように見えますが、object Bar
の apply
定義内でフィールドをoverrideしている箇所は内部的にサブクラスとして扱われるようです。これによって「 final
指定子をつけるとサブクラス化が禁止される」というルール通りの動きになっています。これは scalac
の構文木を表示するオプションをつけて、以下のコードをコンパイルすることで確認できます。
trait Version { val version: Int }
sealed case class Bar() extends Version { override val version = 1 }
object Bar {
def apply(v: Int): Bar = new Bar { override val version = v }
}
コンパイル結果
$ scalac -Vprint:typer example.scala
# 一部省略...
object Bar extends scala.AnyRef with java.io.Serializable {
def <init>(): Bar.type = {
Bar.super.<init>();
()
};
def apply(v: Int): Bar = {
final class $anon extends Bar {
def <init>(): <$anon: Bar> = {
$anon.super.<init>();
()
};
private[this] val version: Int = v;
override <stable> <accessor> def version: Int = $anon.this.version
};
new $anon()
};
case <synthetic> def apply(): Bar = new Bar();
この結果の中で new Bar() { override val version = v }
に相当する箇所では final class $anon extends Bar {...
と書かれているように $anon
という新しいサブクラスが定義されているのがわかります。
というわけで一番最初に戻ると final case class Bar()
と定義したあとに object Bar
内で new Bar() { override val version = v }
と定義することで、内部的にサブクラスとして扱われます。そのためこの状況で final
を使うとコンパイルエラーになり、 sealed
ではサブクラス化ができるスコープが広がるためコンパイルが通るという結果になったのでした。