読者です 読者をやめる 読者になる 読者になる

算譜王におれはなる!!!!

偏りはあると思うけど情報技術全般についてマイペースに書くよ。

ことりんのトレイトとクラスオブジェクトという面白機能

Kotlin

前回に続き、今回もKotlinのオブジェクト指向に関する機能を紹介していきたいと思います。

※下記URLのサイトを参考にしました。英語、または技術的な知識が至らず、内容に誤りが含まれるおそれがありますので、ご了承ください。
※本エントリの8割は参考サイトの翻訳です。残りの2割は私の解釈で加筆や変更を施した構成です。

参考サイト http://confluence.jetbrains.net/display/Kotlin/Classes+and+Inheritance

トレイト

トレイト(trait)は本質的にはメソッド本体を含むインタフェースです(メソッドは任意)。
トレイトの定義とその実装は次のように行います。

trait Foo {
  open fun foo()
}

class Bar() : Foo {
  override fun foo() {
  }
}

オーバライドのルール

クラスがその直近のスーパークラスから同じメンバを複数実装する場合、このメンバをオーバライドし独自の実装を提供しなければなりません。スーパークラスの実装を使用したいときには、superと山括弧(<>←これ)を使います。

open class A() {
  open fun msg() = "a"
  fun a() = "A"
}

trait B {
  open fun msg() = "b" //トレイトは具象クラスも持てる
  open fun b() = "B"
}

open class C() : A(), B {
  override fun msg() = super<A>.msg() + super<B>.msg() //この関数を実装しないとコンパイルエラーとなる
}

class D() : C() {
  override fun msg() = super<A>.msg() //これはエラーとなる。アクセスできるのは、あくまで直近のスーパークラスの実装にだけ
}

AとBを両方とも継承するのは問題ありません。Cにおいて、a()もb()もたったひとつの実装しか継承していないので、これも問題ありません。しかし、Cはmsg()の2つの実装を継承しているので、msg()をオーバライドしなければなりません。そうして独自の実装を提供することにより曖昧さを排除しています。

抽象クラス

Javaのように、クラスやそのメンバは抽象(abstract)として宣言することができます。抽象メンバは、そのクラスにおいては実装を持ちません。したがって子孫が抽象メンバを継承したとき、それは具象メンバとしてカウントされません。次のコードを見てください*1

abstract class A {
  abstract fun f()
}

trait B {
  open fun f() {  }
}

class C() : A, B {
  // f()をオーバライドする必要はありません
}

抽象クラスにはopenアノテーションが不要なことに注意してください。言うまでもないのです。抽象関数についても同様です。

次のコードのように*2、抽象ではないopenなメンバを抽象メンバとしてオーバライドできます。

open class Base {
  open fun f() {}
}

abstract class Derived : Base {
  override abstract fun f()
}

オーバライド可能なプロパティとアクセサ

関数だけでなく、プロパティもopen宣言できます。実際には、プロパティのアクセサがオーバライド可能になることを意味しています。

open class Foo {
  open val prop : String
    get() = "foo"
}

class Bar : Foo {
  override val prop : String
    get() = "bar"
}

アクセサひとつひとつにopenoverrideを付けることもできます。次のコードを見てください*3

open class Foo {
  var prop : String
    get() = "foo"
    open set(value) { }
}

class Bar : Foo {
  var prop : String
    override set(value) { prop = value }
}

委譲

委譲パターン(delegation pattern)は具象継承の代替として良い実績があり、Kotlinではそれをサポートしています。つまりそのための定型コードを書く必要がありません。次のコード*4において、DrivedクラスはBaseトレイトの継承と、そのpublicメソッドの全てを指定したオブジェクトへ委譲することがが可能です。

trait Base {
  fun print()
}

class BaseImpl(x : Int) : Base {
  val x = x
  override fun print() { print(this.x) }
}

class Derived(b : Base) : Base by b

fun main(args : Array<String>) {
  val b = BaseImpl(10)
  Derived(b).print() // prints 10
}

Drivedのスーパークラスリストのby句は、bがDrivedのオブジェクト内部に格納されることと、コンパイラがbへ転送するBaseの全てのメソッドを生成することを示しています。

クラスオブジェクト

KotlinではJavaと異なり、クラスは静的メソッド(static method)を持てません。多くの場合、名前空間レベルの関数(namespace-level function)がそれの良い代替を形成しますが、そうでない場合も少しあります。クラス内部(privateメンバ)にアクセスする場合です。

例えばFactory Methodパターンに従ったコンストラクタの置換は、コンストラクタをprivateにし、コンストラクタを呼び出す関数を提供します。しかし、この関数がここで問題にしているようにクラスの外側に置かれていたら、コンストラクタに一切アクセスできません。

この問題に対応するため(また、その他の興味深い機能を提供するため)、Kotlinではクラスオブジェクト(class object)という概念を導入しています(他の言語における最も近い類似物は、ScalaのCompanion objectです)。大まかに言えば、CクラスのクラスオブジェクトはCと関連したオブジェクトです(オブジェクト宣言的な意味で)。クラスオブジェクトは関連するクラス内部に宣言されるので、そのprivateメンバにアクセスできるのです。Cのクラスオブジェクトは(通常は)Cのインスタンスではありません。例として次のコードを見てください*5

class C() {
  class object {
    fun create() = C()
  }
}

fun main() {
  val c = C.create() // C denotes the class object here
}

最初にあなたはこれがクラスの静的メンバをグルーピングする正当な方法だと思うかも知れません。Javaのように、クラスメンバ(静的メンバ)とインスタンスメンバが混在するような方法と比べて。しかし実際には、両者の振る舞いには重要な違いがあります。クラスオブジェクトはスーパータイプを持つことができます。説明するよりも次のコード*6を見たほうが理解が早いかも知れません。

abstract class Factory<out T> {
  fun create() : T
}

open class C() {
  class object : Factory<C> {
    override fun create() : C = C()
  }
}

fun main() {
  val factory = C // C denotes the class object
  val c = factory.create()
}

注意:クラスオブジェクトは継承できません。
注意:あなたはクラスオブジェクトがKotlinにおける良いシングルトンの実装方法だと思うかも知れませんが、Kotlinのオブジェクト式とオブジェクト宣言 - 算譜王におれはなる!!!!を見てください。

この機能*7に関連するベストプラクティス

Effective Java(Joshua Bloch著)
項目16:Favor composition over inheritance*8
項目17:Design and document for inheritance or else prohibit it*9
項目18:Prefer interfaces to abstract classes*10

他の言語における類似機能

IDEは自動的に委譲メソッドを生成する

Lombokプロジェクトは、Javaにおけるアノテーションでの委譲を実装している。

Scalaはトレイトを持っている。

GroovyCategoryとMixinを持っている。

*1:http://confluence.jetbrains.net/display/Kotlin/Classes+and+Inheritanceのサンプルコードを一部変更

*2:http://confluence.jetbrains.net/display/Kotlin/Classes+and+Inheritanceより引用

*3:http://kotlin-demo.jetbrains.com/で試すとコンパイルエラーとなってしまいます。

*4:http://confluence.jetbrains.net/display/Kotlin/Classes+and+Inheritanceのコードを一部変更

*5:http://confluence.jetbrains.net/display/Kotlin/Classes+and+Inheritanceより引用

*6:http://confluence.jetbrains.net/display/Kotlin/Classes+and+Inheritanceより引用

*7:Kotlinのクラスや継承にまつわる機能のこと。

*8:邦訳:継承よりコンポジションを選ぶ

*9:邦訳:継承のために設計および文書化する、でなければ継承を禁止する

*10:邦訳:抽象クラスよりインタフェースを選ぶ