Edit Page

拡張 (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)は拡張したクラスを実際に変更するわけではありません。 拡張を定義すると、クラスに新たなメンバを挿入するのではなく、そのクラスのインスタンスにおいて、ただ単にその新しい関数をドット表記で呼べるようにするだけです。

拡張関数は 静的に ディスパッチされます。 つまり、どの拡張関数が呼ばれるかは、レシーバの型によりコンパイル時にしられています。 例えば:

fun main() { //sampleStart open class Shape class Rectangle: Shape() fun Shape.getName() = "Shape" fun Rectangle.getName() = "Rectangle" fun printClassName(s: Shape) { println(s.getName()) } printClassName(Rectangle()) //sampleEnd }

この例では、 “Shape”を出力します。呼び出されている拡張関数は パラメータ s として宣言されている型のみに依存し、 それはShapeクラスだからです。

もし、あるクラスにメンバ関数をあり、 さらに、そのメンバ関数と同じレシーバ型、同じ名前を有し、同じ引数を与えられた時に適用可能な拡張関数を定義すると、 常にメンバ関数が優先されます 。例えば:

fun main() { //sampleStart class Example { fun printFunctionType() { println("クラスのメソッド") } } fun Example.printFunctionType() { println("拡張関数") } Example().printFunctionType() //sampleEnd }

このコードはクラスのメソッドを出力します。

しかしながら、異なるシグニチャを持つが同名のメンバ関数を拡張関数がオーバライドすることは全く問題ありません:

fun main() { //sampleStart class Example { fun printFunctionType() { println("クラスのメソッド") } } fun Example.printFunctionType(i: Int) { println("拡張関数 #$i") } Example().printFunctionType(1) //sampleEnd }

Nullableレシーバ

拡張は、nullableなレシーバの型で定義できることに注意してください。 このような拡張は、オブジェクトの変数に対して、その値がnullの場合でも呼び出すことができます。 もしレシーバがnullならthisnullとなります。 だから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)として呼ぶ事が出来ます。

class MyClass { companion object { } // "コンパニオン" と呼ばれるもの } fun MyClass.Companion.printCompanion() { println("コンパニオン") } fun main() { MyClass.printCompanion() }

拡張関数のスコープ

ほとんどの場合、拡張はトップレベル、すなわちパッケージ直下に定義します:

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) と呼ばれ、 拡張関数のレシーバ型のインスタンスは 拡張レシーバ と呼ばれます。

class Host(val hostname: String) { fun printHostname() { print(hostname) } } class Connection(val host: Host, val port: Int) { fun printPort() { print(port) } fun Host.printConnectionString() { printHostname() // Host.printHostname()を呼ぶ print(":") printPort() // Connection.printPort()を呼ぶ } fun connect() { /*...*/ host.printConnectionString() // 拡張関数を呼ぶ } } fun main() { Connection(Host("kotl.in"), 443).connect() //Host("kotl.in").printConnectionString() // error, the extension function is unavailable outside Connection }

ディスパッチレシーバのメンバーと拡張レシーバの名前が衝突する場合には、拡張レシーバが優先されます。 ディスパッチレシーバのメンバを参照するには、限定子付き this の構文を使用することができます。

class Connection {
    fun Host.getConnectionString() {
        toString()         // Host.toString()の呼び出し
        this@Connection.toString()  // Connection.toString()の呼び出し
    }
}

メンバとして宣言する拡張関数は、 open として宣言する事も出来て、 その場合はサブクラスでオーバーライドすることができます。 これは、そのような関数のディスパッチは、ディスパッチレシーバ型に関しては仮想関数的であるけれど、拡張レシーバ型に関しては静的であることを意味します。

open class Base { } class Derived : Base() { } open class BaseCaller { open fun Base.printFunctionInfo() { println("BaseCallerの中のBaseの拡張関数") } open fun Derived.printFunctionInfo() { println("BaseCallerの中のDerivedクラスの拡張関数") } fun call(b: Base) { b.printFunctionInfo() // 拡張関数の呼び出し } } class DerivedCaller: BaseCaller() { override fun Base.printFunctionInfo() { println("DerivedCallerの中のBaseの拡張関数") } override fun Derived.printFunctionInfo() { println("DerivedCallerの中のDerivedの拡張関数") } } fun main() { BaseCaller().call(Base()) // "BaseCallerの中のBaseの拡張関数" DerivedCaller().call(Base()) // "DerivedCallerの中のBaseの拡張関数" - ディスパッチレシーバは仮想的に解決される DerivedCaller().call(Derived()) // "DerivedCallerの中のBaseの拡張関数" - 拡張レシーバは静的に解決される }

可視性についてのメモ

拡張(extension)も同じスコープに関数が定義され時と同様の可視性修飾子の振る舞いとなる。 例えば:

  • ファイルのトップレベルに定義された拡張は、同じファイルのprivateのトップレベル宣言にアクセス出来る
  • 拡張がレシーバ型の外で定義されれば、レシーバのprivateprotectedのメンバにはアクセス出来ない