前回はオブジェクト指向の基本について簡単に説明しました。今回はオブジェクト指向機能の目玉ともいえる「継承」について取り上げます。まず最初に、一般的なオブジェクト指向で使われている継承について簡単に説明します。
継承には「単一継承」と「多重継承」の 2 種類があります。単一継承は、ただ一つのクラスからしか機能を継承することができません。したがって、クラスの階層は下図のような木構造 [*1] で表すことができます。
図 : 単一継承におけるクラスの階層
継承は何段階に渡って行われてもかまいません。たとえばクラス E の場合、スーパークラスが B で、B のスーパークラスが A に設定されています。サブクラスは複数あってもかまいません。
たとえば、A のサブクラスは B, C, D の 3 つがあり、B のサブクラスは E, F の 2 つがあります。上図では、クラス A のスーパークラスはありませんが、ほかのクラスはただ一つのスーパークラスを持っています。オブジェクト指向言語では Smalltalk, Java, Ruby, Scala が単一継承です。
これに対し、多重継承は複数のクラスを継承することができます。このため、クラスの階層は木構造ではなく、下図のようなグラフ [*2] で表すことができます。
図 : 多重継承におけるクラスの階層
クラス E に注目してください。スーパークラスには B と C の 2 つがあります。多重継承では、単一継承と同じくサブクラスを複数持つことができ、なおかつスーパークラスも複数持つことができるのです。C++や CLOS (Common Lisp Object System) は多重継承をサポートしています。スクリプト言語では Perl や Python が多重継承です。
Java や Scala の場合、基本的には単一継承ですが Java であれば「インターフェース (interface)」を使ってメソッドの「仕様」だけを、Scala であれば「トレイト (trait)」を使って「仕様」や「実装」を多重継承できるようになっています。トレイトについては次回以降で詳しく説明します。
一般的なオブジェクト指向言語の場合、継承によって引き継がれる性質は定義されたインスタンス変数やメソッドになります。次の図を見てください。
図 : 一般的な継承
クラス Foo にはインスタンス変数 a, b とリーダーメソッド get_a, get_b が定義されています。次に、クラス Bar を定義します。Bar は Foo を継承し、Bar 固有のインスタンス変数 c とリーダーメソッド get_c を定義します。Foo と Bar のインスタンスを生成すると、上図に示したように、Bar のインスタンスにはクラス Foo で定義された変数 a, b も含まれます。このように、Foo のインスタンス変数が Bar に継承されます。
Foo のインスタンスを生成すると、もちろん変数 a, b は含まれていますが、Bar のインスタンスとメモリを共有することはありません。クラスはオブジェクトの設計図です。設計に共通な部分があったとしても、それから生み出されるインスタンスは別々の実体で、インスタンス変数を共有することはないのです。
クラス Bar にはメソッド get_c しか定義されていませんが、クラス Foo を継承することにより、メソッド get_a と get_b を呼び出すことができます。Bar のインスタンスに対して get_a を呼び出すと、クラス Bar には get_a が定義されていないのでスーパークラス Foo を調べ、そこで定義されている get_a が呼び出されます。もちろん、取り出される値は Bar のインスタンスにある変数 a の値です。このように、Foo のメソッドが Bar に継承されます。
それでは、具体的に Scala の継承を説明しましょう。スーパークラスは class 文で指定します。クラス名の後ろに extends を付けて、その後ろにスーバークラス名を指定します。Scala は単一継承が基本なので、指定できるスーパークラスは一つだけです。継承に必要な設定はこれだけです。
簡単な例として、上図のクラスを実際にプログラムしてみましょう。次の例を見てください。
scala> :paste // Entering paste mode (ctrl-D to finish) class Foo(var a: Int, var b: Int) { println("call Foo") def get_a(): Int = a def get_b(): Int = b def put_a(n: Int): Unit = a = n def put_b(n: Int): Unit = b = n } class Bar(a: Int, b: Int, var c: Int) extends Foo(a, b) { println("call Bar") def get_c(): Int = c def put_c(n: Int): Unit = c = n } // Exiting paste mode, now interpreting. class Foo class Bar scala> val a = new Foo(1, 2) call Foo val a: Foo = Foo@ef718de scala> val b = new Bar(10, 20, 30) call Foo call Bar val b: Bar = Bar@20d19f2c scala> a.get_a() val res0: Int = 1 scala> a.get_b() val res1: Int = 2 scala> b.get_a() val res2: Int = 10 scala> b.get_b() val res3: Int = 20 scala> b.get_c() val res4: Int = 30 scala> a.get_c() ^ error: value get_c is not a member of Foo did you mean get_a or get_b?
クラス Foo のコンストラクタ引数 a, b には var があるので、これらの引数はフィールド変数として扱われます。メソッド get_a と get_b の定義は簡単です。与えられたインスタンスから値を取り出すだけです。次にクラス Bar を定義します。extends の後ろにスーパークラス Foo を指定します。これで Foo のフィールド変数とメソッドを継承することができます。
Bar のコンストラクタ引数では引数 c に var があるので、引数 c がフィールド変数として扱われます。a, b はただの引数になります。フィールド変数 a, b の初期化は Foo のコンストラクタで行っていますね。このメソッドを呼び出すことができれば、わざわざ Bar のコンストラクタで a, b の初期化を行う必要はありません。この場合、extends Foo の後ろにコンストラクタ引数を指定してください。これで Foo のコンストラクタが呼び出されます。
Bar のインスタンスを生成すると、Foo のコンストラクタが呼び出されるので、call Foo が表示されてから call Bar が表示されます。スーパークラスのメソッドは Bar のインスタンスからでも呼び出すことができます。b.get_a は 10 になりますし、自クラスのメソッド b.get_c は 30 になります。なお、Foo のインスタンス a からサブクラスのメソッド get_c は呼び出すことができません。エラーになります。
継承はクラスに新しい機能を追加するだけではなく、メソッドをオーバーライドすることで機能を変更することができます。なお、オーバーライドと多重定義 (オーバーロード) はまったく異なる機能です。混同しないように注意してください。
それでは、簡単な例を示しましょう。フィールド変数の合計値を求めるメソッド sum を定義します。次のリストを見てください。
リスト : メソッドのオーバーライド class Foo(var a: Int, var b: Int) { def get_a(): Int = a def get_b(): Int = b def put_a(n: Int): Unit = a = n def put_b(n: Int): Unit = b = n def sum(): Int = a + b } class Bar(a: Int, b: Int, var c: Int) extends Foo(a, b) { def get_c(): Int = c def put_c(n: Int): Unit = c = n override def sum(): Int = super.sum() + c } object sample0901 { def main(args: Array[String]): Unit = { val a = new Foo(1, 2) val b = new Bar(10, 20, 30) println(a.sum()) println(b.sum()) } }
クラス Foo でメソッド sum を定義します。そして、クラス Bar でメソッド sum をオーバーライドします。オーバーライドはスーパークラスにあるメソッドと同じ名前のメソッドを定義するだけです。Scala の場合、def の前にキーワード override を指定する必要があります。
Scala は Java と同様に super を使ってスーパークラスのメソッドを呼び出すことができます。
super.method(args, ...)
Bar のメソッド sum では、super.sum でスーパークラスのメソッド sum を呼び出して、その結果にフィールド変数 c の値を足し算します。実行結果は次のようになります。
$ scalac sample0901.scala $ scala sample0901 3 60
正常に動作していますね。
Scala の場合、Java と同様にクラス名は型を表す識別子として利用することができます。継承はフィールド変数やメソッドに作用するだけではなく、型にも作用します。サブクラスに属するインスタンスは型も継承されるため、スーパークラスの型として取り扱うことができるのです。インスタンスが属するクラスを判定するメソッド isInstanceOf を使って調べてみましょう。
obj.isInstanceOf[型] => Boolean
次の例を見てください。
scala> class Foo class Foo scala> class Bar extends Foo class Bar scala> class Baz extends Bar class Baz scala> val a = new Foo val a: Foo = Foo@6075b369 scala> val b = new Bar val b: Bar = Bar@42730828 scala> val c = new Baz val c: Baz = Baz@1640f20f scala> a.isInstanceOf[Foo] val res0: Boolean = true scala> b.isInstanceOf[Foo] val res1: Boolean = true scala> c.isInstanceOf[Foo] val res2: Boolean = true scala> a.isInstanceOf[Bar] val res3: Boolean = false scala> b.isInstanceOf[Bar] val res4: Boolean = true scala> c.isInstanceOf[Bar] val res5: Boolean = true scala> a.isInstanceOf[Baz] val res6: Boolean = false scala> b.isInstanceOf[Baz] val res7: Boolean = false scala> c.isInstanceOf[Baz] val res8: Boolean = true
クラス Bar はクラス Foo を継承しています。Foo のインスタンス a は isInstanceOf でチェックすると、Foo では true になり、Bar では false になります。ところが、Bar のインスタンス b は、Bar で true になるのは当然ですが、Foo のサブクラスなので型が継承され、Fooでも true になります。そして、Bar を継承したクラス Baz のインスタンスを作って、それを isInstanceOf でチェックすると、Foo, Bar, Baz のどれでも true になります。
このように、クラスを単一継承してサブクラスを作ると、サブクラスはスーパークラスの部分集合として考えることができます。次の図を見てください。サブクラス Baz は Bar や Foo に含まれているので、そのインスタンスから Bar や Foo のメソッドを呼び出することができるわけです。
クラスを定義するとき、class の前に final を付けると、そのクラスを継承したサブクラスを作ることはできません。つまり、継承を禁止することができます。また、メソッドを定義するとき def の前に final を付けると、サブクラスでオーバーライドすることができなくなります。これらの機能は Java と同じです。
クラスでメソッドを定義するとき、メソッドの型だけを宣言することができます。これを「抽象メソッド (abstract method)」といいます。そして、抽象メソッドを持つクラスを「抽象クラス (abstract class)」といい、new でインスタンスを生成することはできません。なお、Scala はメソッドだけではなくフィールド変数も抽象フィールド変数として定義することができます。
抽象クラスは次のように定義します。
abstract class クラス名 { ... val 名前: 型 var 名前: 型 def 名前(引数: 型, ...): 型 ... }
抽象フィールド変数と抽象メソッドは名前と型を指定するだけで、= 以降の式や値は書きません。抽象フィールド変数、抽象メソッドを持つクラスは class の前に必ず abstract を宣言してください。なお、Java と違って、メソッドの定義で abstract をつける必要はありません。また、抽象メソッドがないクラスでも abstract を宣言することができます。もちろん、そのクラスは抽象クラスになります。
抽象クラスは継承されることを前提としたクラスで、抽象フィールド変数や抽象メソッドはサブクラスにおいて具体的に定義されます。抽象クラスでは、サブクラス共通のメソッドを定義します。このとき、抽象メソッドを呼び出してもかまいません。サブクラスのインスタンスが生成されるとき、そのサブクラスでは抽象メソッドが具体化されているはずなので、実際にはサブクラスのメソッドが呼び出されることになります。
簡単な例を示しましょう。
scala> abstract class Foo { def foo(): Unit } class Foo scala> class Foo1 extends Foo { def foo(): Unit = println("Foo!!") } class Foo1 scala> val a = new Foo1 val a: Foo1 = Foo1@61191222 scala> a.foo() Foo!!
クラス Foo のメソッド foo は抽象メソッドなので、Foo は抽象クラスになります。abstract を付けないとエラーになります。クラス Foo1 は Foo を継承していて、抽象メソッド foo をオーバーライドしています。抽象メソッドをオーバーライドする場合は、キーワード override を省略してもかまいません。もちろん、override を付けても大丈夫です。これで、Foo の抽象メソッドをすべてオーバーライドしたので、クラス Foo1 は具象クラスとなり、new でインスタンスを生成することができ、メソッド foo を呼び出すことができます。
それでは簡単な例題として、図形の面積を求めるプログラムを作ってみましょう。次のリストを見てください。
リスト : 図形のクラス abstract class Figure { def kindOf: String def area: Double def print(): Unit = println(kindOf + ": area = " + area) } class Triangle(val altiude: Double, val base: Double) extends Figure { def kindOf: String = "Triangle" def area: Double = altiude * base / 2 } class Rectangle(val width: Double, val height: Double) extends Figure { def kindOf: String = "Rectangle" def area: Double = width * height } class Circle(val radius: Double) extends Figure { def kindOf: String = "Circle" def area: Double = radius * radius * Math.PI } object sample0902 { def main(args: Array[String]): Unit = { val a = new Triangle(2, 2) val b = new Rectangle(2, 2) val c = new Circle(2) a.print() b.print() c.print() } }
クラス Figure は抽象クラスです。メソッド kindOf と area が抽象メソッドで、kindOf は図形の種類を文字列で返し、area は図形の面積を計算して返します。kindOf と area はサブクラスで定義します。
print は図形の種別と面積を表示するメソッドです。ここで、抽象メソッド kindOf と area を呼び出しています。実際には、print を呼び出したインスタンスが属するクラスのメソッドが呼び出されます。つまり、ポリモーフィズムにより適切なメソッドが呼び出されるわけです。
クラス Triangle, Rectangle, Circle は Figure を継承します。どのクラスも抽象メソッド kindOf と area を具体化しています。なお、スーパークラスの抽象メソッドをすべて具体化しないと、そのサブクラスも抽象クラスになるため、コンパイルでエラーになります。ご注意ください。
それでは実行例を示します。
$ scalac sample0902.scala $ scala sample0902 Triangle: area = 2.0 Rectangle: area = 4.0 Circle: area = 12.566370614359172
正常に動作していますね。
Java や Scala では、サブクラスのインスタンスをスーパークラスのデータ型に変換することができます。型変換のことを「キャスト」といいます。特に、サブクラスをスーパークラスに変換することを「アップキャスト」といい、スーパークラスのインスタンスをサブクラスの型に変換することを「ダウンキャスト」といいます。
Java や Scala の場合、サブクラスのインスタンスをスーパークラスの変数 (メソッドの引数) や配列などに代入するとき、自動的にアップキャストが行われます。コンパイルエラーは発生しません。これに対し、スーパークラスのインスタンスをサブクラスの変数などに代入するとき、プログラマが明示的にキャストしないとコンパイルエラーになります。
簡単な例を示しましょう。
scala> :paste // Entering paste mode (ctrl-D to finish) class Foo { def display(): Unit = println("Foo") } class Bar1 extends Foo { override def display(): Unit = println("Bar1") } class Bar2 extends Foo { override def display(): Unit = println("Bar2") } class Baz { def display(): Unit = println("Baz") } // Exiting paste mode, now interpreting. class Foo class Bar1 class Bar2 class Baz scala> val a = new Foo val a: Foo = Foo@66223d94 scala> val b = new Bar1 val b: Bar1 = Bar1@3c9ef6e9 scala> val c = new Bar2 val c: Bar2 = Bar2@16f7f59f scala> val d = new Baz val d: Baz = Baz@54463380 scala> val a1: Foo = b // アップキャスト val a1: Foo = Bar1@3c9ef6e9 scala> val a2: Foo = c // アップキャスト val a2: Foo = Bar2@16f7f59f scala> a.display() Foo scala> a1.display() // ポリモーフィズム Bar1 scala> a2.display() // ポリモーフィズム Bar2
各クラスのインスタンスを生成して変数 a, b, c, d にセットします。Bar1, Bar2 は Foo のサブクラスなので、Foo の変数 a1 に b を、a2 に c を代入することができます。これがアップキャストです。そして、a からメソッド display を呼び出すと Foo と表示されますが、a1, a2 から display を呼び出すと、ポリモーフィズムが働いて Bar1, Bar2 の display が呼び出され、Bar1 と Bar2 が表示されます。
scala> a1.isInstanceOf[Bar1] val res3: Boolean = true scala> val b1: Bar1 = a1.asInstanceOf[Bar1] val b1: Bar1 = Bar1@3c9ef6e9 scala> b1.display() Bar1
逆に、Bar1 の変数 b1 に a1 を代入するには明示的にキャストする必要があります。これがダウンキャストです。Scala の場合、isInstanceOf[Bar1] でチェックして、true であればダウンキャストすることができます。型変換はメソッド asInstanceOf[型] で行います。これで変数 b1 に a1 を代入することができます。そして、b1 からメソッド display を呼び出すと Bar1 と表示されます。
scala> d.isInstanceOf[Foo] ^ warning: fruitless type test: a value of type Baz cannot also be a Foo val res5: Boolean = false scala> val a3: Foo = d.asInstanceOf[Foo] java.lang.ClassCastException: class Baz cannot be cast to class Foo (Baz and Foo are in unnamed module of loader scala.tools.nsc.interpreter.IMain$TranslatingClassLoader @2d7e1102) ... 32 elided
それから、Foo と継承関係のない Baz のインスタンスは、キャストしても Foo や Bar の変数に代入することはできません。ご注意くださいませ。
次に、図形のオブジェクトをリストにまとめて格納することを考えてみましょう。Triangle, Rectangle, Circle は型が違うので、同じリストに格納することはできません。この場合、スーパークラス Figure に型変換すると同じリストに格納することができます。次のリストを見てください。
リスト : キャスト (2) object sample0903 { def sumOfArea(xs: List[Figure]): Double = { var sum = 0.0 for (x <- xs) sum += x.area sum } def main(args: Array[String]): Unit = { val a = new Triangle(2, 2) val b = new Rectangle(2, 2) val c = new Circle(2) a.print() b.print() c.print() println(sumOfArea(List(a, b, c))) } }
$ scalac sample0903.scala $ scala sample0903 Triangle: area = 2.0 Rectangle: area = 4.0 Circle: area = 12.566370614359172 18.566370614359172
関数 sumOfFigure は図形の面積の合計値を求めます。型変換した場合、サブクラスの情報は失われるため、サブクラス独自のメソッドを呼び出すことはできません。型変換したスーパークラスのメソッドしか利用できませんが、ポリモーフィズムによりサブクラスのメソッドが呼び出されるため、図形の面積を正しく計算することができます。
前回は継承について簡単に説明しました。今回は「トレイト (trait)」について取り上げます。
Scala の「トレイト」は、Java の「インターフェース」と比較するならば、実装を持つことが可能なインターフェース、ということができます。Java のインターフェースはメソッドの仕様 (抽象メソッド) だけを定義した抽象クラスの一種ですが、「多重継承」できるところが他のクラスと異なるところです。
Scala のトレイトは、基本的には Java のインターフェースと同じですが、大きな違いは実装を持たせることができるところです。メソッドだけではなくフィールド変数も持たせることができます。Scala のトレイトは多重継承が簡単にできるように機能を制限した「クラス」と考えることができます。
クラスを多重継承するとき複雑な問題を引き起こす場合がありますが、その問題点について把握しておくと、トレイトの理解が深まると思います。そこで、まず最初に多重継承の問題点について説明します。
一般的なオブジェクト指向の場合、継承によって引き継がれる性質は定義されたデータ (インスタンス変数など) やメソッドになります。これを「実装の継承」と呼びます。また、インスタンス変数を継承することを特別に「属性の継承」と呼ぶ場合があります。
多重継承を行う場合、異なる性質や機能を持つクラスを継承することがあります。たとえば、クラス Foo にはメソッド method_a があり、クラス Bar にはメソッド method_b があるとしましょう。この 2 つのメソッドはまったく異なる働きをするとします。ここで、メソッド method_a はインスタンス変数 x を使っていて、method_b も x を使っていると、多重継承で問題が発生します。
一般的な多重継承で、クラス Foo と Bar を継承してクラス Baz を作成した場合、クラス Baz のインスタンスに変数 x は一つしか存在しません。メソッド method_a と method_b は一つしかない x を使うことになります。この場合、どちらかのメソッドは正常に動作しないでしょう。これでは多重継承する意味がありませんね。
また、多重継承ではインスタンス変数だけではなく、メソッド名が衝突する場合もあります。このように、多重継承では名前の衝突が発生する危険性があるのです。それから、多重継承にはもう一つ問題点があります。それはクラスの階層構造が複雑になることです。
単一継承の場合、クラスの階層は木構造になりますが、多重継承ではグラフになります。木構造の場合、クラスの優先順位は簡単に決めることができますが、グラフになると優先順位を決めるためのアルゴリズムは複雑になり、それを理解するのは難しくなります。多重継承は強力な機能ですが、使うときには十分な注意が必要になるのです。
ちなみにC++の場合、多重継承したクラスに同名のメソッドがある場合、どちらを呼び出すのか明確に指定しないとコンパイルでエラーとなります。またC++はメンバ変数も継承されるため、変数名の衝突も発生します。この場合も、どちらの変数を使用するのか明確に指定しないとコンパイルエラーとなります。
このほかにも、多重継承ではいろいろな問題が発生するため、それを解決するためにC++ではいろいろな機能が用意されています。ところが、それらの機能がC++をいっそう複雑な言語にしていると M.Hiroi には思えてなりません。C++はコンパイラ型の言語で、なによりも効率を重視するため、複雑な言語仕様になるのは避けられないのかもしれません。
これらの問題を回避するため、属性を継承するスーパークラスは一つだけに限定して、あとのスーパークラスは実装だけを継承するという方法があります。これを Mix-in といいます。
具体的には、インスタンス変数を定義せずにメソッドだけを記述したクラスを用意します。属性の継承は単一継承になりますが、実装のみを記述したクラスはいくつ継承してかまいません。一つのクラスに複数の実装を混ぜることから Mix-in と呼ばれています。
なお、Mix-in は特別な機能ではなく、多重継承を使いこなすための方法論にすぎません。多重継承を扱うことができるプログラミング言語であれば Mix-in を行うことが可能です。この Mix-in という方法を言語仕様に取り込んでいるのが Ruby です。Mix-in を下図に示します。
図 : Mix-in
クラス C はクラス B を継承していて、そこにクラス Mixin A が Mix-in されています。クラス D もクラス B を継承していますが、Mix-in されているクラスは Mixin B となります。
多重継承の問題点は Mix-in ですべて解決できるわけではありませんが、クラスの階層構造がすっきりとしてわかりやすくなることは間違いありません。Mix-in は多重継承を使いこなす優れた方法だと思います。
Scala のクラスは単一継承ですが、トレイトは多重継承ができるので、Scala でも Mix-in を使うことができます。また、トレイトは型として使用することもできます。つまり、クラスをトレイトの型に変換することで、ポリモーフィズムを有効に活用することができるわけです。
それでは、具体的にトレイトの使い方を説明しましょう。トレイトの定義は trait 文で行います。trait の構文を示します。
trait 名前 extends クラス or トレイト [with トレイト] { ... }
trait の後ろに名前を指定します。トレイトは extends で他のクラスを継承することができます。継承するクラスがない場合は extends にトレイトを指定することができます。トレイトは with を使って指定します。多重継承する場合は、"with トレイト" を複数記述します。
そして、ブロックの中でフィールド変数やメソッドなどを宣言します。実装を定義することも可能です。ただし、コンストラクタ引数を持つことはできません。補助コンストラクタも定義することはできません。また、トレイト単独でインスタンスを生成することはできません。ご注意くださいませ。
クラスでトレイトを継承する場合も extends や with を使います。指定方法は trait と同じです。with を使って複数のトレイトを継承することができます。
簡単な例を示しましょう。次のリストを見てください。
リスト : トレイトの使い方 class Foo(val a: Int = 1, val b: Int = 2) { def foo(): Unit = println("Foo") def method(): Unit = println("Foo method") } trait Bar1 { def bar1(): Unit = println("Bar1") def method(): Unit = println("Bar1 method") } trait Bar2 { def bar2(): Unit = println("Bar2") def method(): Unit = println("Bar2 method") } class Baz extends Foo with Bar1 with Bar2 { override def method(): Unit = super.method() } object sample0904 { def main(args: Array[String]): Unit = { val a = new Baz println(a.a) println(a.b) a.foo() a.bar1() a.bar2() a.method() } }
クラス Foo にはフィールド変数 a, b とメソッド method、トレイト Bar1, bar2 にはメソッド method が定義されています。Foo と Bar1, Bar2 を継承するクラス Baz を定義する場合、同名のメソッド method があるので、method を Baz でオーバーライドしてください。オーバーライドしないと、どのメソッドを呼び出したらよいかコンパイラが決定できずにエラーとなります。
実行結果は次のようになります。
$ scalac sample0904.scala $ scala sample0904 1 2 Foo Bar1 Bar2 Bar2 method
継承したメソッド foo, bar1, bar2 を呼び出すことができるのは当然ですが、3 つのクラスにある method は、どのクラスのメソッドが呼び出されるのでしょうか。表示された Bar2 method から、トレイト Bar2 のメソッドが呼び出されたことがわかります。
多重継承でメソッドを探索する場合、簡単に言うと with の右側から順番に行われ、最後に extends で指定したクラスを探します。そして、最初に見つかったメソッドを実行します。したがって、with でトレイトの順番を逆にすると、今度は Bar1 の method が実行されます。
なお、トレイト Bar1 と Bar2 にスーパークラスが設定されている場合、メソッドの探索はもっと複雑になります。Scala の場合、メソッドの優先順位は「線形化」という方法で決定します。「線形化」については本稿の範囲を超えるので説明は割愛します。興味のある方は調べてみてください。
特定のスーパークラスのメソッドを呼び出したい場合は、super の後ろに [クラス名] を指定します。たとえば、Baz で Foo の method を呼び出したい場合は次のように指定します。
override def method(): Unit = super[Foo].method
これで Foo の method を呼び出すことができます。
それでは簡単な例として、前回作成した図形のプログラムをトレイトを使って書き直してみましょう。次のリストを見てください。
リスト : 図形のクラス (トレイト版) trait Figure { def kindOf: String def area: Double def print(): Unit = println(kindOf + ": area = " + area) } class Triangle(val altiude: Double, val base: Double) extends Figure { def kindOf: String = "Triangle" def area: Double = altiude * base / 2 } class Rectangle(val width: Double, val height: Double) extends Figure { def kindOf: String = "Rectangle" def area: Double = width * height } class Circle(val radius: Double) extends Figure { def kindOf: String = "Circle" def area: Double = radius * radius * Math.PI } object sample0905 { def sumOfArea(xs: List[Figure]): Double = { var sum = 0.0 for (x <- xs) sum += x.area sum } def main(args: Array[String]): Unit = { val a = new Triangle(2, 2) val b = new Rectangle(2, 2) val c = new Circle(2) a.print() b.print() c.print() println(sumOfArea(List(a, b, c))) } }
$ scalac sample0905.scala $ scala sample0905 Triangle: area = 2.0 Rectangle: area = 4.0 Circle: area = 12.566370614359172 18.566370614359172
最初に図形を操作するトレイト Figure を定義します。この中でメソッド kindOf, area, を宣言し、print の実装を定義します。次に図形を表すクラス Triangle, Rectangle, Circle を定義します。ここで extends で Figure を継承します。そして、各クラスで Figure のメソッドを実装します。
クラスは継承したトレイトの型にアップキャストすることができます。どのクラスも Figure を継承しているので型 List[Figure] のリストに格納することができます。
面積の合計を求める関数 sumOfArea は引数に Figure 型のリストを受け取ります。そして、Figure に定義されているメソッドを使ってインスタンスを操作することができます。sumOfArea はメソッド area を呼び出していますが、ポリモーフィズムの働きにより各クラスのメソッド area が呼び出されるので図形の面積を正しく求めることができます。
もうひとつ簡単な例題として、値の大小関係を比較するための演算子を定義するトレイト Ord を作ってみましょう。次のリストを見てください。
リスト : Ord トレイト trait Ord { def <(that: Any): Boolean def <=(that: Any): Boolean = this < that || this == that def >(that: Any): Boolean = !(this < that) && this != that def >=(that: Any): Boolean = !(this < that) }
Scala の演算子はメソッドとして定義されています。たとえば、1 + 2 は (1).+(2) と呼び出すこともできます。1. は浮動小数点数として扱われるので、1 はカッコで囲んでください。== と != はメソッド equals をオーバーライドすれば使用することができます。演算子 < を抽象メソッドとすると、演算子 <=, >, >= は <, ==, != で定義することができます。
それでは図形の抽象クラス Figure に Ord を実装してみましょう。値の大小関係は面積で比較します。次のリストを見てください。
リスト : Ord の使用例 trait Ord { def <(that: Any): Boolean def <=(that: Any): Boolean = this < that || this == that def >(that: Any): Boolean = !(this < that) && this != that def >=(that: Any): Boolean = !(this < that) } abstract class Figure extends Ord { def kindOf: String def area: Double def print(): Unit = println(kindOf + ": area = " + area) override def equals(other: Any): Boolean = other match { case that: Figure => area == that.area case _ => false } def <(other: Any): Boolean = other match { case that: Figure => area < that.area case _ => false } } class Triangle(val altiude: Double, val base: Double) extends Figure { def kindOf: String = "Triangle" def area: Double = altiude * base / 2 } class Rectangle(val width: Double, val height: Double) extends Figure { def kindOf: String = "Rectangle" def area: Double = width * height } class Circle(val radius: Double) extends Figure { def kindOf: String = "Circle" def area: Double = radius * radius * Math.PI } object sample0906 { def insertSort(xs: List[Figure]): List[Figure] = { def insertElement(x: Figure, xs: List[Figure]): List[Figure] = xs match { case Nil => List(x) case y::ys => if (x < y) x::y::ys else y::insertElement(x, ys) } xs match { case Nil => Nil case y::ys => insertElement(y, insertSort(ys)) } } def main(args: Array[String]): Unit = { val a = new Triangle(5, 5) val b = new Rectangle(2, 2) val c = new Circle(2) for (x <- insertSort(List(a, b, c))) x.print() } }
$ scalac sample0906.scala $ scala sample0906 Rectangle: area = 4.0 Triangle: area = 12.5 Circle: area = 12.566370614359172
抽象クラス Figure でトレイト Ord を継承します。このクラスでメソッド equals をオーバーライドして、メソッド < を実装します。どちらのメソッドも match 式で引数 other の型が Figure であることを確認し、メソッド area を呼び出して大小関係をチェックします。
関数 insertSort は拙作のページ パターンマッチング で作成した単純挿入ソートの型を Int から Figure に変えたものです。これで図形を格納したリスト (List[Figure]) をソートすることができます。
Java の場合、どんなクラスでも明示的にキャストすればインターフェースへ型変換することができますが、Scala はそうではありません。isInstanceOf[トレイト] の返り値が true であれば、asInstancOf[トレイト] でキャストすることができますが、返り値が false の場合はキャストすることはできません。
簡単な例を示しましょう。
scala> trait foo { def bar(): Unit } trait foo scala> class Baz1 extends foo { def bar(): Unit = println("oops!") } class Baz1 scala> class Baz2 class Baz2 scala> val a = new Baz1 val a: Baz1 = Baz1@231b35fb scala> val b = new Baz2 val b: Baz2 = Baz2@72fb989b scala> val c: foo = a val c: foo = Baz1@231b35fb scala> b.isInstanceOf[foo] val res0: Boolean = false scala> val d: foo = b.asInstanceOf[foo] java.lang.ClassCastException: class Baz2 cannot be cast to class foo (Baz2 and foo are in unnamed module of loader scala.tools.nsc.interpreter.IMain$TranslatingClassLoader @1136b469) ... 32 elided
トレイト foo を定義します。クラス Baz1 は foo を継承し、メソッド bar を定義します。クラス Baz2 は foo を継承せず、メソッド bar も定義していません。次に、Baz1 と Baz2 のインスタンスを生成します。Baz1 は foo を継承しているので、キャストしなくても foo に型変換することができます。Baz2 は foo を継承していないので、b.isInstanceOf[foo] の返り値は false になり、b.asInstanceOf[foo] でキャストしても失敗します。