トレイト
トレイトとはJavaの抽象クラスのようなものであり、Scalaのオブジェクト指向プログラミングにおけるモジュール化(プログラム分割)の中心的な概念です。
Scalaのトレイトはクラスと比較して以下のような特徴があります。
- 複数のトレイトを1つのクラスやトレイトにミックスイン(javaで言うinterfaceの実装のこと)できる。
- 直接インスタンス化できない(トレイト単一ではインスタンス化できない。単独ではできる)。new TraitA with TraitB のようにすればトレイト単独でもインスタンス化できる。これを行う際には全ての抽象メンバーが実装されている必要がある。
- クラスパラメータ(コンストラクタの引数)を取ることができない。
Scalaでは複数クラスの継承はできません。しかし、クラスをトレイトにすることで複数継承を行うことができます。これをミックスインと呼びます。
トレイトはトレイト単体でインスタンス化できません。トレイトをインスタンス化するときはトレイトを継承したクラスを作成し、これをインスタンス化します。
トレイトはコンストラクタ引数を取ることができません。しかし、トレイトに抽象メンバーを持たせることで値を渡すことができます。
トレイトは菱形継承問題を線形化という方法で解決しています。
菱形継承問題とは
trait TraitA {
def greet(): Unit
}
trait TraitB extends TraitA {
def greet(): Unit = printl("hoge")
}
trait TraitC extends TraitA {
def greet(): Unit = println("foo")
}
class ClassA extends TraitB with TraitC
このようなプログラムを作成した場合にTraitBとTraitCのメソッドの実装が衝突し、TraitBとTraitCどちらのメソッドを実行するべきなのか曖昧になる問題のことです。
Scalaではoverride指定なしの場合のメソッド定義の衝突はエラーになります。
1つの解決方法は片方のメソッドにoverrideをを指定し、継承したクラスではsuperを使用してoverrideされていない方のメソッドを呼び出すという方法です。
この時両方のメソッドを飛び出したい場合はsuper[TraitB].greet()、super[TraitC].greet()のようにして呼び出すことです。しかし、この解決方法は継承関係が複雑になると大変になります。
この問題の解決法としてScalaでは線形化を使用しています。
線形化機能とはトレイトがミックスインされた順番をトレイトの継承順番とみなすことです。
以下に線形化を使用した菱形継承問題の解決を示します。
trait TraitA {
def greet(): Unit
//= println("Trait A")
}
trait TraitB extends TraitA {
//superを使うことで親トレイトを使用できます。
//super.greet()
override def greet(): Unit = println("Trait B")
}
trait TraitC extends TraitA {
//superを使うことで親トレイトを使用できます。
//super.greet()
override def greet(): Unit = println("Trait C")
}
//ミックスインの順番を変えると出力結果も変化する。
class ClassA extends TraitB with TraitC
class ClassB extends TraitC with TraitB
(new ClassA).greet()
(new ClassB).greet()
このように線形化の機能によりミックスインされた全てのトレイトの処理を簡単に呼び出すことができます。
線形化によるトレイトの積み重ねの処理をScala用語では積み重ね可能トレイト(Stackable Trait)と呼びます
Scalaにはクラスやトレイトの中で自分自身の型にアノテーションを記述できます。これを自分型アノテーション(self type annotations)や自分型(self type)などと呼びます。 自分型はまるで継承のように見えます。しかし、継承とは違います。
trait Greeter {
def greet(): Unit
}
trait Robot {
self: Greeter =>
def start(): Unit = greet()
}
trait HelloGreeter extends Greeter {
override def greet(): Unit = println("Hello!")
}
val r = new Robot with HelloGreeter
r.start()
自分型を使う場合は、抽象トレイトを指定し、後から実装を追加するという形になります。このように後から利用するモジュールの実装を与えることを依存性の注入と呼ぶことがあります。 自分型は直接継承する場合と比べて以下の違いがあります。
- 親トレイトのメンバーを外から呼び出すことができない。自分型で呼び出しているトレイト内では可能
循環参照が可能です。以下に循環参照の例を示します。
trait Robot { self: Greeter => def name: String def start(): Unit = greet() } trait Greeter { self: Robot => def greet(): Unit = println("helo") }
これを継承に置き換えることはできません。
トレイトの"val"の初期化順序はスーパークラスから順に行われます。よって以下のような場合意図せぬ結果が発生する場合があります。
trait A {
val foo: String
}
trait B extends A {
//lazy val bar = foo + "World"
//def bar = foo + "world"
val bar = foo + "World"
}
class C extends /*{val foo = "Hello"} with*/ B {
val foo = "Hello"
def printBar(): Unit = println(bar)
}
(new C).printBar()
これを回避するためには初期化を遅延させます。処理を遅延させるには"lazy val"もしくはdefを使います。 ただし、lazy valはvalに比べて若干処理が重く、複雑な呼び出しでデッドロックが発生する場合があります。 しかし、valの代わりにdefを使用すると毎回値を計算してしまうという問題があります。 けれども、大概の場合において両方共大きな問題にはならない場合が多いのでどちらを使っても大丈夫でしょう。
またもう一つの回避方法として事前定義があります。これはフィールドの初期化をスーパークラスよりも先に行う方法です。 しかし、トレイト初期化問題は継承されるトレイト側で解決したほうが良いことが多いのでこのこの機能はあまり見られないそうです。