15日目:トレンドなトレイト
アドベントカレンダー15日目の今日はトレイトという機能に注目したいと思います。トレイトとは実装を持ったインタフェースのようなものです。これはJVM言語のScala(まさにトレイトという名前の機能)や、Java SE 8(予定)にもある機能です。インタフェースが実装を持つとことで便利なこともありますが、問題もあるのではないかと心配になりますね。そこらへんを見て行きましょう。
抽象クラス
トレイトの話題の前に抽象クラスを紹介します。抽象クラス、Javaプログラマならご存知ですね。抽象関数を持つクラスのことを抽象クラスと言い、そのクラスはインスタンス化できません。サブクラスを作成するために使用します。抽象関数とは実装を持たず、関数シグネチャのみを宣言した関数です。具体的な実装はサブクラスで定義します。ポリモーフィズム(多態性)を実現するための重要な仕組みです。
抽象クラスの概念のおさらいはここまでにして、実際にKotlinコードを見てみましょう。クラスや関数を抽象として宣言するにはアノテーション abstract を伴う必要があります。
abstract class Greeter() { abstract fun greet(name : String) }
抽象関数は実装を持ってはいけません。また、抽象関数を持つクラスは必ず抽象クラスとして定義します。まぁここらへんはJavaと同じですね。
抽象クラスは継承して使うわけですが、アノテーション open が付いていなくても継承することが可能です(抽象関数もopenでなくてもオーバライド可能です)。継承の例を示します。
class GreeterImpl() : Greeter() { override fun greet(name : String) { println("Hello, $name!") } }
トレイト
トレイト(trait)は、Javaで言うところのインタフェースのようなものです。抽象関数を持ち、クラスは複数のトレイトを実装*1することができます。そしてJavaのインタフェースとの最大の違いは、トレイトはデフォルトの実装として具象関数を持つことができます*2。
トレイトの定義
トレイトを定義するには、キーワード trait を使用します。それに続けてトレイト名を指定します。クラスに似ている宣言ですが、トレイトはコンストラクタを持ちません。そのため、トレイト名の後にプライマリ・コンストラクタを表す括弧はあってはなりません。
trait Greeter { fun greet(name : String) { println("Hello, $name!") } fun greetInLoudVoice(name : String) }
この例では関数 greet は実装を持っています。関数 greetInLoudVoice は実装なしなので抽象関数です。トレイトで宣言された実装を持たない関数はデフォルトで abstract です。また、具象関数はデフォルトで open です。
トレイトの実装
トレイトを実装するにはクラス名とトレイト名の間にセミコロンを置きます。継承と同じですね。このトレイト Greeter を実装する例を見ます。
class GreeterImpl() : Greeter { override fun greetInLoudVoice(name : String) { println("HELLO, ${name.toUpperCase()}!!!"); } }
関数 greet をオーバライドしていないのがわかると思いますが、デフォルト実装が提供されているので、これについてコンパイラは文句を言いません。
複数のトレイトを実装(ミックスイン)
トレイトはJavaのインタフェースのように複数個を同時に実装できます。次の例は、具象関数を持った2つのトレイトを1つのクラスが実装する例です。
trait Hoge { fun foo() = "foo" } trait Fuga { fun bar() = "bar" } class NiceClass() : Hoge, Fuga
お気づきかも知れませんが、これはJavaの世界では追放されている多重継承に似ています。多重継承は、それが形成する構造の複雑さ、曖昧さなどの問題と付き合わなければならない諸刃の剣です。そのためJavaではこれを限定的に許可しています。それがインタフェースです。インタフェースは状態も実装も持たないので安全です。ではトレイトはどうでしょうか。トレイトは状態を持ちません。つまり純粋な具象関数定義を促します。また、曖昧さの問題は次に紹介するオーバライドのルールによって解決しています。
オーバライドのルール
多重継承が可能であると、関数名の衝突の扱いが重要になります。次のコードは特に問題はありません。
trait Hoge { fun piyo() = "hoge" } trait Fuga { fun piyo() } class NiceClass() : Hoge, Fuga fun main(args : Array<String>) { println(NiceClass().piyo()) // hoge }
同一シグネチャの関数を持った2つのトレイトを実装していますが、一方は具象関数であるのに対してもう一方は抽象関数です。そのため NiceClass のインスタンスに対して関数 piyo を呼び出したとき、トレイト Hoge の提供する実装が採用されるのは明らかです。では、次のようにトレイト Fuga 側でも実装を持ったときにはどうなるでしょうか。
trait Hoge { fun piyo() = "hoge" } trait Fuga { fun piyo() = "fuga" } class NiceClass() : Hoge, Fuga
これはコンパイルエラーになります。NiceClass で piyo を実装してくれ、それは複数の実装を持ってるから!とコンパイルは文句を言ってきます。もしこのコードをコンパイラが受け入れていたら、NiceClass の piyo の実装について曖昧さが生じてしまいます。では、素直にコードを修正します。
class NiceClass() : Hoge, Fuga { override fun piyo() = "nice" }
これでコンパイルは通ります。NiceClass の piyo を呼び出すと "nice" を返します。
デフォルト実装を呼び出す
オーバライド前の関数を使用したい場合があるでしょう。そんなときは次のようにすればオーバライド前の関数を呼び出せます。
trait Hoge { fun piyo() = "hoge" } class NiceClass() : Hoge { override fun piyo() = super.piyo() }
Javaと同じようにキーワード super 、ドット、関数名で簡単に呼び出せます。
継承した関数の名前が衝突した場合はどのようにしてオーバライド前の実装を呼び出せるのでしょうか。Kotlinにはこの解決策も用意されています。
trait Hoge { fun piyo() = "hoge" } trait Fuga { fun piyo() = "fuga" } class NiceClass() : Hoge, Fuga { override fun piyo() = "${super<Hoge>.piyo()} & ${super<Fuga>.piyo()}" } fun main(args : Array<String>) { println(NiceClass().piyo()) // hoge & fuga }
superに続けて<型>を指定することで曖昧さを排除することに成功します。
委譲はいいじょう(いいぞ〜)!
継承は強力な機能ですが、サブクラスはスーパクラスの実装に依存してしまう危険性があります。そこで、継承を避けて委譲(デリゲーション)を使用することでクラスのカプセル化が保たれます。Javaの世界でこのテクニックは一般的*3ですが、どうしても定型コードが多くなり、見通しが悪くなってしまいがちです。
Kotlinでは言語レベルでこの問題を解決します。委譲を簡単に実現するための機能が言語に組み込まれています。
trait Greeter { fun greet(name : String) } class GreeterImpl() : Greeter { override fun greet(name : String) { println("Hello, $name!") } } class Person(g : Greeter) : Greeter by g
トレイト Greeter とその実装クラス GreeterImpl、それからクラス Person を定義しました。人には挨拶してもらいたいところなので、GreeterImpl を継承するのもアリなんですが、そうしてしまうと全人類の挨拶が GreeterImpl に支配されてしまいます。上記の例では Greeter型のオブジェクトをPerson が持つことになります。いわゆる has-a 関係というやつです。
肝心なのはPersonのクラス宣言で、継承のような書き方をしている部分がありますが、これが自動で委譲コードが生成される仕組みです。これによりPersonがGreeterになっているかのように見えます。つまり、is-a 関係です。継承のようにPersonはGreeterが持っている関数と同一シグネチャの関数を持つことになります。実際にはその関数内では単にGreeter型オブジェクトの g に処理を委譲しているに過ぎません。
ぐだぐだ言ってきましたが、重要なのは、PersonはGreeterを継承しているかのように見えますし、実際、そのように振る舞います。継承と違うポイントは、型と実装が分離していること。そして具象クラス(例ではGreeterImpl)の内部状態にはアクセスできないことと、具象クラスは静的に決定されるものではないことです。
最後にクラス Person の使用例を示します。
trait Greeter { fun greet(name : String) } class GreeterImpl() : Greeter { val something : String = "hogehoge" override fun greet(name : String) { println("Hello, $name!") } } class AnotherGreeterImpl() : Greeter { override fun greet(name : String) { println("Hi, $name!") } } class Person(g : Greeter) : Greeter by g fun main(args : Array<String>) { val foo : Person = Person(GreeterImpl()) foo.greet("Delegation") // Hello, Delegation! // println(foo.something) // compile ERROR!!! val bar = Person(AnotherGreeterImpl()) bar.greet("Kotlin") // Hi, Kotlin! }
まとめと次回予告
今日は抽象クラスから始めて、トレイト、委譲について学びました。抽象クラスやトレイトでポリモーフィズムを実現します。抽象クラスはJavaのそれとほぼ同じです。トレイトは具象関数を持てるインタフェースと言いましたが、抽象クラスからインスタンス変数の存在を消し去ったものと言えるでしょう。トレイトは複数実装(ミックスイン)することが可能です。その際に起こる名前衝突などの曖昧さを排除するためのオーバライドのルールも学びました。最後にトレイトを上手く使うテクニックとして委譲を紹介しました。
明日はNULL安全についてお話しします。NULL安全によりKotlinでは基本的にNullPointerExceptionは起こりません。この例外を見るとげんなりしますよね。明日、それを倒す方法を伝授します。
日記
今日は寒いし雨降ってるっぽいから外出したくないなぁ(´・ω・`)