0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

scalaのfinalとsealedの使い分け—フィールドをoverrideする場合

Last updated at Posted at 2020-02-12

この記事では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 Barapply 定義内でフィールドをoverrideしている箇所は内部的にサブクラスとして扱われるようです。これによって「 final 指定子をつけるとサブクラス化が禁止される」というルール通りの動きになっています。これは scalac の構文木を表示するオプションをつけて、以下のコードをコンパイルすることで確認できます。

example.scala
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 ではサブクラス化ができるスコープが広がるためコンパイルが通るという結果になったのでした。

  1. wartremoverのFinalCaseClassが有効になっていると sealed も許可されないはずなのですが…なぜか通ります。大元の状況としてはcirceの @JsonCodec を使っていて、プロジェクトの設定(build.sbt)ではFinalCaseClassは除外されているため、この状況のみで有効かもしれません。 ← FinalCaseClassでも sealed は許可されていました(詳細はコメントにて)

  2. Scalaスケーラブルプログラミング 第3版 pp. 197-198, pp. 278-279

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?