S-JIS[2010-12-25/2014-03-22] 変更履歴
Scalaのトレイトは、Javaのインターフェースに実装が書けるような感じのもの。
|
||
Javaはクラスの多重継承は出来ないが、複数のインターフェース(interface)を1つのクラスに実装(implements)することが出来るようになっている。
そのクラスを呼び出す(使う)側からすれば、そのインターフェース経由でメソッドが呼べるということなので、分かり易くて便利だ。
しかしそのクラスを作る(用意する)側からすると、インターフェースで宣言されている全てのメソッドの中身をコーディングしなければならないので、面倒。
(インターフェースでは、メソッドの宣言は出来ても、その中身(本体)を記述することが出来ない為(JDK1.7以前))
例えどのクラスでも同じようなコーディングになるとしても、インターフェースを実装した各クラスで個別にコーディングしなければならない。
また、既存インターフェースにメソッドを追加したら、そのインターフェースをimplementsしている全てのクラスで新メソッドを実装する必要が出てくる。
Scalaでもクラスの多重継承は出来ないが、インターフェースの代わりにトレイト(trait)がある。
トレイトも(インターフェースと同様に)1つのクラスで複数のトレイトをミックスインすることが出来る。
(インターフェースは「実装(implements)する」と言うが、トレイトは「ミックスイン(mixin)」と呼ぶらしい)
トレイトとインターフェースの大きな違いは、トレイトは実装(メソッドの本体やフィールド)を持つことが出来る、ということ。
したがって、どのクラスでも同じようなコーディングになる内容は、トレイトに実装しておけばよい。
※Javaでも、JDK1.8でインターフェースにメソッド本体を記述できるようになった。→デフォルトメソッド[2014-03-22]
(ただし、JDK1.8でもインターフェース内にフィールド(変数)は定義できない)
トレイトの定義方法は、クラスの定義方法(特に抽象クラス)と同様。
〔アクセス修飾子〕 trait トレイト名 〔extends 親トレイトまたは親クラス〔with 他トレイト〕…〕{ type 型名 = 型 val 定数名 = 初期値 var 変数名 = 初期値 def メソッド名(引数…) : 戻り型 = 本体 //抽象メンバー type 型名 val 定数名:型 var 変数名:型 def メソッド名(引数…) : 戻り型 //コンストラクター本体 }
トレイトは(クラス定義と異なり)、コンストラクターで引数を持つことが出来ない。
引数を持った補助コンストラクター(def
this(〜))を定義することも出来ない。
トレイトをミックスインしたクラスを定義するには、withを使う。
ただし親クラスが無い場合、1つ目のトレイトはextendsを使う。
親クラス無し | 親クラスあり | |
トレイト 1つ |
class クラス名 extends トレイト { //コンストラクター } class クラス名 extends AnyRef with トレイト {
//コンストラクター
}
|
class クラス名 extends 親クラス with トレイト { //コンストラクター } |
トレイト 2つ |
class クラス名 extends トレイト1 with トレイト2 { //コンストラクター } class クラス名 extends AnyRef with トレイト1 with トレイト2 {
//コンストラクター
}
|
class クラス名 extends 親クラス with トレイト1 with トレイト2 { //コンストラクター } |
事前初期化 [2011-01-08] |
class クラス名 extends {
//事前初期化
} with トレイト {
//コンストラクター
}
class クラス名 extends {
//事前初期化
} with トレイト1 with トレイト2 {
//コンストラクター
}
|
Scalaの例 | Java相当 | 備考 |
---|---|---|
trait A { } class C extends A { } |
public interface A { } public class C implements A { } |
|
trait A { val Z = 123 def f() } class C extends A { def f() = { println("c" + Z) } } |
public interface A { public static final int Z = 123; public void f(); } public class C implements A { @Override public void f() { System.out.println("c" + Z); } } |
Java(JDK1.6)では、インターフェースのメソッドを実装した際にも@Overrideアノテーションを付ける。(JDK1.5までは付けない) Scalaでは抽象メソッドを実装した場合はoverrideは付けない。(付けるとエラーになる) |
trait A { def f() } trait B { def f() } class C extends A with B { def f() = println("c") } scala> val a:A = new C a: A = C@1ee9771 scala> a.f() c scala> val b:B = new C b: B = C@179bd59 scala> b.f() c |
public interface A { public void f(); } public interface B { public void f(); } public class C implements A, B { @Override public void f() { System.out.println("c"); } } final A a = new C(); a.f(); final B b = new C(); b.f() |
2つのトレイトで同じメソッドを宣言していて、その両方をミックスインしたクラスの例。 変数の型がどちらのトレイトであっても、同じメソッド(実体)が呼び出される。 |
trait A { def f() = println("a") } trait B { def f() } class C extends A with B { } scala> val b:B = new C b: B = C@d64315 scala> b.f() a scala> class D extends B with A defined class D scala> new D().f() a |
public class A { public void f() { System.out.println("a"); } } public interface B { public void f(); } class C extends A implements B { } final B b = new C(); b.f() |
片方のトレイトでメソッド本体も実装して、別のトレイトで同じシグニチャーの抽象メソッドを宣言した場合、両方をミックスインしたクラスでは実装されたメソッドが呼び出せる。 両方ともトレイトなので、ミックスインさせる順番(extendsとwith)を入れ替えても同じになる。(左記のクラスD) Javaではこういう事は出来ない。 |
trait A { def f() = println("a") } trait B { def f() = println("b") } scala> class C extends A with B
<console>:7: error: overriding method f in trait A of type ()Unit;
method f in trait B of type ()Unit needs `override' modifier
class C extends A with B
^
class C extends A with B { override def f() = super.f() } scala> new C().f() b |
別々のトレイトで両方ともメソッド本体まで実装した場合 (異なるトレイトがたまたま同じシグニチャーのメソッドを実装していた場合)、 それらをミックスインしたクラスではそのメソッドを必ずオーバーライドする必要がある。 ただしオーバーライドしたメソッド内から「super」で親トレイトのメソッドを呼び出すことが出来る。 この場合呼び出されるメソッドは、一番最後(一番右側)のwithで継承されたトレイトのものとなる。 (だったらわざわざオーバーライドしなくても一番右のトレイトのメソッドを呼び出してくれればいいじゃん、と思わなくも無いが、 メソッド定義の予期せぬ衝突を防ぐためにもコンパイルエラーになるようになっているのかな?) →super[]によるクラス指定 |
|
trait A { println("a-constructor") } trait B { println("b-constructor") } class C extends A with B { println("c-constructor") } scala> new C a-constructor b-constructor c-constructor res100: C = C@19a5ebc |
インスタンス化の際にコンストラクターが呼ばれる順番は、一番最初(一番左)で宣言されたトレイトから。 |
また、newでインスタンス生成する際もトレイトをミックスインすることが出来る。
Scalaの例 | Java相当 | 備考 |
---|---|---|
trait A { def f() } scala> val a = new A { | def f() = println("zzz") | } a: java.lang.Object with A = $anon$1@1095a1 scala> a.f() zzz |
public interface A { public void f(); } final A a = new A() { @Override public void f() { System.out.prinln("zzz"); } }; a.f(); |
トレイト単独でインスタンス化することが出来る。 Javaの無名クラス(匿名クラス)に相当。 |
trait A { def a() = println("aaa") } trait B { def b() = println("bbb") } scala> val c = new A with B c: java.lang.Object with A with B = $anon$1@7918b0 scala> c.a() aaa scala> c.b() bbb |
Javaの無名クラスの構文では複数のインターフェースを実装したインスタンスを作ることは出来ないが、 トレイトでは出来る。 |
|
trait A { def f() //抽象メソッド } class C extends A { def f() = println("ccc") } trait B extends A { abstract override def f() = { println("b start") super.f() println("b end") } } scala> val c = new C c: C = C@f6a7cd scala> c.f() ccc scala> val d = new C with B d: C with B = $anon$1@17ac43d scala> d.f() b start ccc b end |
トレイトAをミックスインしたクラスCが有るとき、 Aを継承したトレイトBを作り、Cのインスタンス生成時にBをミックスインすることで、 Bのメソッドを呼ぶように出来る。 つまり、“インスタンス生成時に既存処理に新処理を追加する”という振る舞いが出来る。 なお、このとき、トレイトAのメソッドは抽象メソッドである必要があり(本体を書いてはいけない)、 トレイトBではメソッドに「abstract override」を付ける必要がある。 参考: じゅんいち☆かとうさんのScalaにAOPをやらせてみる |
トレイト内の抽象メンバーの初期化順序について。[2011-01-08]
通常のクラスの定義方法では、トレイトで抽象メンバーを定義してそれをトレイト内で使おうとすると、まだ初期化されていないので使えない。
実際に値を定義するクラスのコンストラクターよりトレイトのコンストラクターの方が先に動く為、下記のsumが初期化される時点ではa1・a2は0のままだから。
trait A { val a1:Int //抽象メンバー val a2:Int //抽象メンバー val sum = a1 + a2 println("A" + (a1, a2, sum)) }
class C extends A { val a1 = 123 val a2 = 456 println("C" + (a1, a2 ,sum)) } scala> new C().sum A(0,0,0) C(123,456,0) res5: Int = 0
そこで、クラスの定義方法を事前初期化するようにすると、クラスのコンストラクターの方が先に呼ばれて初期化されるようになる。
なお、この方法の場合、extends直後のブロック内ではメンバーの定義以外の記述(例えばprintln()を書くこと)が出来ない。
下記のdummyは(実行順を確認したかったので)println()を呼ぶ為に入れてみたもの。
この時点ではsumはスコープ外のようで、参照しようとするとコンパイルエラーになる。
class C extends { //事前初期化
val a1 = 123
val a2 = 456
val dummy = println("C" + (a1, a2))
} with A
scala> new C().sum
C(123,456)
A(123,456,579)
res6: Int = 579
trait A { val a:Int //抽象メンバー println("A " + a) } trait B { val b:Int //抽象メンバー println("B " + b) } |
||
class C extends A with B { println("C1" + (a, b)) val a = 123 val b = 456 println("C2" + (a, b)) } scala> new C A 0 B 0 C1(0,0) C2(123,456) res10: C = C@c6fd6e |
class C extends { val a = 123 val dummy = println("C1 " + a) } with A with B { val b = 456 println("C2" + (a, b)) } scala> new C C1 123 A 123 B 0 C2(123,456) res11: C = C@5cd7f9 |
class C extends { val a = 123 val b = 456 val dummy = println("C1" + (a, b)) } with A with B { println("C2" + (a, b)) } scala> new C C1(123,456) A 123 B 456 C2(123,456) res12: C = C@1a65a18 |
事前初期化の方法は、クラス定義側の書き方に依存してしまう。
lazy valを使えば、トレイト側の定義だけで完結できる。
trait A { val a1:Int //抽象メンバー val a2:Int //抽象メンバー lazy val sum = { println("sum"+(a1,a2)); a1 + a2 } }
class C extends A { val a1 = 123 val a2 = 456 println("C" + (a1, a2)) } scala> val c = new C C(123,456) c: C = C@18e400b scala> c.sum sum(123,456) res19: Int = 579
メソッド内でsuperを使うと継承元(ミックスインされたトレイト)のメソッドを呼び出すことが出来るが、
superではクラス名(トレイト名)を指定することも出来る。[2011-04-02]
コーディング例 | 実行結果 | 備考 |
---|---|---|
class A1 { def f() = println("A1") } trait A2 extends A1 { override def f() = println("A2") } class A extends A1 with A2 { def g() = { f() super.f() super[A1].f() super[A2].f() } } |
scala> new A().g() A2 A2 A1 A2 |
「super[クラス(トレイト)名]」とすると、オーバーライドされた間のメソッドを跳ばして、指定したクラスのメソッドを呼び出すことが出来る。 |
class A extends A2 { def g() = { f() super.f() super[A1].f() super[A2].f() } } |
<console>:11: error: A1 does not name a parent class of class A super[A1].f() ^ |
ただしsuperで指定できるのは直接の親クラス(自分が直接extends・withしているクラス・トレイト)だけであり、親の親クラスは指定できない。 |
class A1 { def f() = println("A1") } trait A2 { def f() = println("A2") } class A extends A1 with A2 |
<console>:7: error: overriding method f in class A1 of type ()Unit; method f in trait A2 of type ()Unit needs `override' modifier class A extends A1 with A2 ^ |
これで同名メソッドを呼び分けることが出来るかも。と思ったが、 そもそもトレイト同士に継承関係が無い場合は同名メソッドがあるトレイトをミックスインすることは出来ない。 と思ったが…↓[2011-09-13] |
class A1 { def f() = println("A1") } trait A2 { def f() = println("A2") } class A extends A1 with A2 { override def f() = { super[A1].f() super[A2].f() } } |
継承関係の無いクラス・トレイト同士に同名メソッドがある場合でも、オーバーライドすればミックスインできる。[2011-09-13] (あおいさんにご指摘いただきました) |
toStringメソッドの様に、各トレイトで独自実装されていてその内のどれを呼び出すか決まっている時に便利そう。
trait MapLike[A, +B, +This <: MapLike[A, B, This] with Map[A, B]] extends PartialFunction[A, B] with IterableLike[(A, B), This] with Subtractable[A, This] { 〜 override def toString = super[IterableLike].toString 〜 }
トレイトと自分型アノテーションを使用して、ソースファイルを分割する(ような)ことが出来る。[2011-08-25]
例えばC++の場合、クラスの宣言と定義が分かれている(分けられる)ので、定義部分を複数のファイルに書くことが出来る。(最後にリンクする段階で全ての定義が揃えばいい)
(あるいは定義を書いた別ファイルを#includeするという方法も採れる)
Javaはクラス定義の中に全て書かないといけないので、ソースファイルを分割することは出来ない。
Scalaでも基本的にはクラス定義の中に全てを書かないといけないが、
トレイトに自分型アノテーションでクラスを指定することにより、そのクラスの実装の一部がトレイトで書ける、
つまりソース分割をしたような状態になる。
C++ | C++ #include |
Scala | 備考 |
---|---|---|---|
test.hclass A { public: int aaa(); int aaa2(); int zzz(); int zzz2(); }; |
ヘッダーファイル。 「クラスにどういうメンバーがいるか」という宣言だけ行う。 Scalaにはヘッダーファイルは無い(が、ただ単に何も実装しないトレイトを定義すればいいという話も^^;[/2011-08-26])。 →ヘッダーファイルをエミュレートする方法(by kmizuさん)[2011-08-26] |
||
testa.cpp#include "test.h" int A::aaa() { return zzz2(); } int A::aaa2() { return 100; } |
test.cpp#include "test.h" int A::aaa() { return zzz2(); } int A::aaa2() { return 100; } #include "testz.h" |
testa.scalaclass A extends AnyRef with Z { def aaa() = { zzz2() } def aaa2() = { 100 } } |
Zをミックスインしているので Aの中からZのメンバーが使用できる。 |
testz.cpp#include "test.h" int A::zzz() { return aaa2(); } int A::zzz2() { return 22; } |
testz.hint A::zzz() { return aaa2(); } int A::zzz2() { return 22; } |
testz.scalatrait Z { self : A => def zzz() = { aaa2(); } def zzz2() = { 22 } } |
「self(自分)はAである」という宣言をしているので Zの中からAのメンバーが使用できる。 |
#include <iostream> #include "test.h" using namespace std; int main(int argc, char* argv[]) { A a; cout << a.aaa() + a.zzz() << endl; } |
object M { def main(args: Array[String]) { val a = new A println(a.aaa() + a.zzz()) } } |
||
$ c++ -c testa.cpp $ c++ -c testz.cpp $ c++ testm.cpp testa.o testz.o |
$ c++ -c test.cpp $ c++ testm.cpp test.o |
> scalac -d classes M.scala ^ testa.scala testz.scala |
Scalaの例では、AとZが互いを参照しているので 同時にコンパイルする必要がある。 (REPLでAとZをそのまま定義しようとすると順番にしか実行できないので エラーになる。 objectの内部クラスにしたり:pasteで書いたりする必要がある。[2011-08-26] |
ただ、やはりクラス内のprivateメンバーにはトレイトからアクセスできない(protectedメンバーにはアクセスできる)とか、他にも自分型アノテーションの制限とかがある。
なので単なるソースファイルの分割というわけにはいかない。
しかし“あるクラスの一部のフィールドやメソッドだけツールを使って生成したい”なんていう場合には、この方法が有効かも。