拡張 (extension)
Kotlinは、クラスやインターフェースを、継承したりDecoratorのようなデザインパターンを使用せずとも、新しい機能で拡張する能力を提供します。 これは、 拡張 (extension) と呼ばれる特別な宣言を介して行われます。
例えば、あなたが変更出来ないようなサードパーティーのライブラリのクラスやインターフェース用に関数を書く事が出来ます。 そのような関数は、まるで元のクラスのメソッドであるかのように、普通の呼び方で呼び出す事が出来ます。 このメカニズムを拡張関数(extension function)と呼びます。 また、既存のクラスに新たなプロパティを追加する事を可能とする拡張プロパティ(extension properties)もあります。
拡張関数
拡張関数を宣言するには レシーバの型 (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 list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'swap()' 中の 'this' は 'list' の値となる
もちろん、任意の MutableList<T>
についてもこの関数は考える事が出来るので、ジェネリックにもできます:
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' はリストに対応する
this[index1] = this[index2]
this[index2] = tmp
}
関数名の前でジェネリック型のパラメータを宣言する必要があります。 そうするとレシーバ型の式で使用できるようになります。 ジェネリクスについての詳細はジェネリック関数を参照してください。
拡張は 静的 に解決される
拡張機能(extension)は拡張したクラスを実際に変更するわけではありません。 拡張を定義すると、クラスに新たなメンバを挿入するのではなく、そのクラスのインスタンスにおいて、ただ単にその新しい関数をドット表記で呼べるようにするだけです。
拡張関数は 静的に ディスパッチされます。 つまり、どの拡張関数が呼ばれるかは、レシーバの型によりコンパイル時にしられています。 例えば:
この例では、 “Shape”を出力します。呼び出されている拡張関数は パラメータ s
として宣言されている型のみに依存し、
それはShape
クラスだからです。
もし、あるクラスにメンバ関数をあり、 さらに、そのメンバ関数と同じレシーバ型、同じ名前を有し、同じ引数を与えられた時に適用可能な拡張関数を定義すると、 常にメンバ関数が優先されます 。例えば:
このコードはクラスのメソッドを出力します。
しかしながら、異なるシグニチャを持つが同名のメンバ関数を拡張関数がオーバライドすることは全く問題ありません:
Nullableレシーバ
拡張は、nullableなレシーバの型で定義できることに注意してください。
このような拡張は、オブジェクトの変数に対して、その値がnullの場合でも呼び出すことができます。
もしレシーバがnull
ならthis
がnull
となります。
だからnullableなレシーバ型に対して対して拡張を定義する時は、
コンパイルエラーを避ける為に関数の本体でthis == null
のチェックを実行する事を推奨しています。
これにより、null をチェックせずに Kotlin で toString() を呼び出すことができます。チェックは拡張関数内で行われるからです。
fun Any?.toString(): String {
if (this == null) return "null"
// nullチェックの後だと、 'this' は非nullable型に自動キャストされるので、
// 下記の toString() は Any クラスのメンバであると解決される
return toString()
}
拡張プロパティ
関数と同様、Kotlinは拡張プロパティ(extension properties)をサポートしています。
val <T> List<T>.lastIndex: Int
get() = size - 1
拡張機能は実際にはクラスにメンバを挿入しないので、 拡張プロパティがバッキングフィールドを持つ効率的な方法はありません。 これが 初期化子(initializer)が、拡張プロパティでは許可されていない 理由です。 拡張プロパティの挙動は、明示的にゲッター/セッターを作ることによってのみ定義することができます。
例:
val Foo.bar = 1 // エラー:初期化子は拡張プロパティでは許可されていない
コンパニオンオブジェクトの拡張
クラスにコンパニオンオブジェクトが定義されている場合は、 コンパニオンオブジェクトの拡張関数と拡張プロパティを定義することもできます。 それらはクラス名だけを限定子(qualifier)として呼ぶ事が出来ます。
拡張関数のスコープ
ほとんどの場合、拡張はトップレベル、すなわちパッケージ直下に定義します:
package org.example.declarations
fun List<String>.getLongestString() { /*...*/}
そのような拡張を宣言しているパッケージの外で使用するには、 それを呼び出し箇所でインポートする必要があります:
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}
詳細については、インポートを参照してください。
メンバとして拡張関数を宣言
クラス内にも、別のクラスの拡張を宣言することができます。 そのような拡張の中では、複数の 暗黙的なレシーバが存在する事になります。 それらのレシーバのメンバは、修飾子なしでアクセスできる事になります。 拡張が宣言されているクラスのインスタンスは ディスパッチレシーバ (dispatch receiver) と呼ばれ、 拡張関数のレシーバ型のインスタンスは 拡張レシーバ と呼ばれます。
ディスパッチレシーバのメンバーと拡張レシーバの名前が衝突する場合には、拡張レシーバが優先されます。
ディスパッチレシーバのメンバを参照するには、限定子付き this
の構文を使用することができます。
class Connection {
fun Host.getConnectionString() {
toString() // Host.toString()の呼び出し
this@Connection.toString() // Connection.toString()の呼び出し
}
}
メンバとして宣言する拡張関数は、 open
として宣言する事も出来て、
その場合はサブクラスでオーバーライドすることができます。
これは、そのような関数のディスパッチは、ディスパッチレシーバ型に関しては仮想関数的であるけれど、拡張レシーバ型に関しては静的であることを意味します。
可視性についてのメモ
拡張(extension)も同じスコープに関数が定義され時と同様の可視性修飾子の振る舞いとなる。 例えば:
- ファイルのトップレベルに定義された拡張は、同じファイルの
private
のトップレベル宣言にアクセス出来る - 拡張がレシーバ型の外で定義されれば、レシーバの
private
やprotected
のメンバにはアクセス出来ない