拡張 (extension)
Kotlinは、C#やGosuと似ていて、クラスを継承したりDecoratorのようなデザインパターンを使用せずとも、クラスを新しい機能で拡張する能力を提供します。 これは、 拡張 と呼ばれる特別な宣言を介して行われます。Kotlinは、 拡張関数 と 拡張プロパティ をサポートしています。
拡張関数
拡張関数を宣言するには レシーバタイプ (receiver type) を関数名の前に付ける必要があります。
次の例では、 swap
関数を MutableList<Int>
に追加しています:
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' がリストに対応する
this[index1] = this[index2]
this[index2] = tmp
}
拡張関数内での this キーワードは、レシーバオブジェクト(ドットの前に渡されたもの)に対応しています。これで、この関数を任意の MutableList<Int>
からでも呼べるようになりました:
val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // 'swap()' 中の 'this' は値 '1' を保持する
もちろん、任意の MutableList<T>
についてこの関数は理にかなっており、ジェネリックにもできます:
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' はリストに対応する
this[index1] = this[index2]
this[index2] = tmp
}
関数名の前でジェネリック型のパラメータを宣言すると、レシーバ型の式で使用できるようになります。ジェネリック関数を参照してください。
拡張は 静的 に解決される
拡張機能は拡張したクラスを実際に変更するわけではありません。拡張を定義すると、クラスに新たなメンバを挿入するのではなく、そのクラスのインスタンスにおいて、ただ単にその新しい関数をただドット付きで呼べるようになるだけです。
拡張関数は 静的に 処理される、つまり、それらはレシーバの型による仮の存在ではないということを強調しておきたいです。これは、関数が呼び出されたときの式の型によって、呼び出される拡張関数が決定されるのであって、実行時の式の評価によるのではないことを意味します。例えば:
open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
println(c.foo())
}
printFoo(D())
この例では、 "c"を出力します。呼び出されている拡張関数は C
クラスのパラメータ c
の宣言型にのみ依存するためです。
もし、あるクラスがメンバ関数を持つうえ、さらに、同じレシーバ型、同じ名前を有し、与えられた引数を受容可能な拡張関数が宣言されると、 常にメンバが優先されます 。例えば:
class C {
fun foo() { println("member") }
}
fun C.foo() { println("extension") }
C
型の任意の c
から c.foo()
を呼べば、"extension" ではなく、 "member" と表示します。
しかしながら、異なる署名を持つが同名のメンバ関数を拡張関数がオーバライドすることは全く問題ありません:
class C {
fun foo() { println("member") }
}
fun C.foo(i: Int) { println("extension") }
C().foo(1)
の呼び出しで "extension" を表示します。
Null許容レシーバー
拡張は、null許容なレシーバの型で定義できることに注意してください。このような拡張は、その値がnullの場合でも、オブジェクト変数で呼び出すことができ、かつその本体内で this == null
をチェックすることができます。これにより、null をチェックせずに Kotlin で toString() を呼び出すことができます。チェックは拡張関数内で行われます。
fun Any?.toString(): String {
if (this == null) return "null"
// nullチェックの後だと、 'this' は非null型に自動キャストされるので、
// 下記の toString() は Any クラスのメンバであると解決される
return toString()
}
拡張プロパティ
関数と同様、Kotlinは拡張プロパティをサポートしています。
val <T> List<T>.lastIndex: Int
get() = size - 1
注意してほしいのは、拡張機能は、実際にはクラスにメンバを挿入しないので、拡張プロパティがバッキングフィールドを持つ効率的な方法がない、ということです。これが 初期化子が、拡張プロパティでは許可されていない 理由です。この挙動は、明示的にゲッター/セッターを作ることによって定義することのみができます。
例:
val Foo.bar = 1 // エラー:初期化子は拡張プロパティでは許可されていない
コンパニオンオブジェクトの拡張機能
クラスにコンパニオンオブジェクトが定義されている場合は、コンパニオンオブジェクトの拡張関数とプロパティを定義することもできます。
class MyClass {
companion object { } // "Companion" と呼ばれる
}
fun MyClass.Companion.foo() {
// ...
}
ちょうどコンパニオンオブジェクトの普通のメンバと同じように、それらは修飾子としてクラス名のみを使用して呼び出すことができます。
MyClass.foo()
拡張関数のスコープ
ほとんどの場合、トップレベル、すなわちパッケージ直下に拡張を定義します:
–>
package foo.bar
fun Baz.goo() { ... }
そのような拡張を宣言しているパッケージの外で使用するには、それを呼び出し箇所でインポートする必要があります:
package com.example.usage
import foo.bar.goo // "goo" という名前で全ての拡張をインポートする
// または
import foo.bar.* // "foo.bar" から全てインポートする
fun usage(baz: Baz) {
baz.goo()
)
詳細については、インポートを参照してください。
メンバとして拡張関数を宣言
クラス内では、別のクラスの拡張を宣言することができます。そのような拡張の中には、複数の 暗黙的なレシーバ_があります。修飾子なしでアクセスできるオブジェクトのメンバです。拡張が宣言されているクラスのインスタンスは _ディスパッチレシーバ (dispatch receiver) と呼ばれ、拡張関数のレシーバ型のインスタンスは 拡張レシーバ と呼ばれます。
class D {
fun bar() { ... }
}
class C {
fun baz() { ... }
fun D.foo() {
bar() // D.bar を呼ぶ
baz() // C.baz を呼ぶ
}
fun caller(d: D) {
d.foo() // 拡張関数を呼ぶ
}
}
ディスパッチレシーバのメンバーと拡張レシーバの名前が衝突する場合には、拡張レシーバが優先されます。ディスパッチレシーバのメンバを参照するには、修飾子付き this
の構文を使用することができます。
class C {
fun D.foo() {
toString() // D.toString() を呼ぶ
this@C.toString() // C.toString() を呼ぶ
}
メンバとして宣言された拡張関数は、 open
として宣言され、サブクラスでオーバーライドすることができます。これは、そのような関数のディスパッチは、ディスパッチレシーバ型に関しては仮想的であるが、拡張レシーバ型に関しては静的であることを意味する。
open class D {
}
class D1 : D() {
}
open class C {
open fun D.foo() {
println("D.foo in C")
}
open fun D1.foo() {
println("D1.foo in C")
}
fun caller(d: D) {
d.foo() // 拡張関数を呼ぶ
}
}
class C1 : C() {
override fun D.foo() {
println("D.foo in C1")
}
override fun D1.foo() {
println("D1.foo in C1")
}
}
C().caller(D()) // 出力: "D.foo in C"
C1().caller(D()) // 出力: "D.foo in C1" - ディスパッチレシーバは仮想的に解決される
C().caller(D1()) // 出力: "D.foo in C" - 拡張レシーバは静的に解決される
動機
Javaでは、 "*Utils" という名前のクラス( FileUtils
、 StringUtils
など)をよく使っていました。有名な java.util.Collections
は、同じ品種に属します。そして、これらのUtils-クラスについての不快な部分は、それらを使用するコードが次のようになることです:
// Java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list))
これらのクラス名は常に邪魔になってきます。static インポートを使用すると、これをこうすることができます:
// Java
swap(list, binarySearch(list, max(otherList)), max(list))
これは少しだけマシですが、IDEの強力なコード補完の助けを全くまたはほんの少ししか得られません。次のようにできるならば、それはとても良いでしょう:
// Java
list.swap(list.binarySearch(otherList.max()), list.max())
でも、 List
クラスの中に考えられるすべてのメソッドを実装したいというわけではありませんよね?このようなときに拡張が私たちを助けてくれます。