Kotlin 拡張関数(Extension functions)

概要

Kotlinは、クラスを継承したりDecoratorのようなデザインパターンを使ったりしなくても、クラスを新しい機能で拡張できる機能を提供している。これは拡張(extension)宣言によって行う。

たとえば、変更できないサードパーティライブラリ(Third-party library)のクラスに新しい関数を書くことができる。このような関数は、元のクラスのメソッドであるかのように通常の方法で呼び出せる。この仕組みを拡張関数(Extension functions)という。既存クラスに新しいプロパティを定義できる拡張プロパティ(Properties)もある。

また、既に存在する基本クラスにも関数を追加できる。たとえば、次のようにIntStringのような基本型(クラス)にも関数を追加できるという意味である。

fun String.hello() = println("Hello $this")

fun main() {
    "devkuma".hello()
}

拡張関数

拡張関数を宣言するには、拡張される型を表すレシーバー型(receiver type、拡張対象となるクラス)を関数名の前に付ける。定義すると次のようになる。

fun レシーバー型.関数名(パラメータ): 戻り値型 { 実装部 }

次の例では、MutableList<Int>にスワップ関数を追加している。

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this'はMutableList<Int>に該当する。
    this[index1] = this[index2]
    this[index2] = tmp
}

拡張関数内のthisキーワードは、レシーバーオブジェクト(関数名のドットの前に渡されるオブジェクト)に該当する。これにより、MutableList<Int>に宣言した関数を呼び出せるようになる。

val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'swap()'内の'this'は'list'の値を持っている。

Generic classの拡張関数

Generic classに対して拡張関数を追加するには、次のように定義できる。

fun クラス名.関数名(引数型): 戻り値型 { 実装部 }

上の関数をIntだけでなく他のオブジェクトでも活用できるように、ジェネリックを使ってMutableList<T>として一般化できる。

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this'はMutableList<T>に該当する。
    this[index1] = this[index2]
    this[index2] = tmp
}

拡張は静的に解決される(Extensions are resolved statically)

拡張は、実際に拡張対象のクラスを変更するわけではない。拡張を定義してもクラスに新しいメンバーを挿入するのではなく、その型の変数に対してドット(.)記法で新しい関数を呼び出せるようにするだけである。

拡張関数は静的にディスパッチされる。つまり、レシーバー型に対して仮想的ではない。呼び出される拡張関数は、実行時にその式を評価した結果の型ではなく、関数が呼び出される式の型によって決定される。

例:

open class Shape
class Rectangle: Shape()

fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
    println(s.getName())
}

fun main() {
    printClassName(Rectangle())
}

Output:

Shape

この例では、呼び出される拡張関数がShapeクラスであるパラメータsの宣言された型だけに依存するため、Shapeが出力される。

メンバー関数を持つクラスに、同じレシーバー型、同じ名前、同じ引数の拡張関数が定義されると、メンバー関数が実行される。

たとえば:

class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType() { println("Extension function") }

fun main() {
    Example().printFunctionType()
}

Output:

Class method

この例ではClassメソッドが出力される。

ここで拡張関数の名前は同じでも、引数が異なるメンバー関数をオーバーロードする場合は、別の関数を追加することになるため、拡張関数が実行される。

class Example2 {
    fun printFunctionType() { println("Class method") }
}

fun Example2.printFunctionType(i: Int) { println("Extension function #$i") }

fun main() {
    Example2().printFunctionType(1)
}

Null許容レシーバー(Nullable receiver)

拡張はnullableなレシーバー型として定義することもできる。このような拡張は、値がnullの場合でもオブジェクト変数から呼び出すことができ、内部ではthis == nullを確認できる。

そのため、拡張関数内で既に確認が行われるので、nullを確認しなくてもtoString()を呼び出せる。

fun Any?.toString(): String {
    if (this == null) return "null"
    // nullかどうか確認した後、'this'はnullではない型に自動変換されるため、
    // 下の'toString()'はAnyクラスのメンバー関数として解決される。
    return toString()
}

次の例を実行してみよう。

fun main() {
    println(null.toString())
}

Output:

null

nullに対してtoString()を実行しているが、エラー(NPE)は発生せず、nullが出力されることが分かる。

拡張プロパティ(Extension properties)

Kotlinは関数をサポートするのと同じように、拡張プロパティもサポートしている。

val <T> List<T>.lastIndex: Int
    get() = size - 1

拡張は実際にメンバーをクラスへ挿入するわけではないため、拡張プロパティにバッキングフィールド(backing field)を持たせる効率的な方法はない。このため、拡張プロパティでは初期化が許可されない。したがって、getter/setterを明示的に提供することでのみ定義できる。

val House.number = 1 // エラー: 初期化は拡張プロパティでは許可されない。

Companionオブジェクト拡張(Companion object extensions)

クラスにCompanionオブジェクトが定義されている場合、Companionオブジェクトに対する拡張関数およびプロパティも定義できる。Companionオブジェクトの通常のメンバーと同じように、クラス名だけを修飾子として使って呼び出せる。

class MyClass {
    companion object { }  // このコードを"Companion"という。
}

fun MyClass.Companion.printCompanion() { println("companion") }

fun main() {
    MyClass.printCompanion()
}

Output:

companion

拡張のスコープ(Scope of extensions)

ほとんどの場合、拡張はパッケージ直下のトップレベル(top level)で定義する。

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()
}

詳しくはインポート(import)を参照してほしい。

拡張をメンバーとして宣言する(Declaring extensions as members)

あるクラス内で、別のクラスに対する拡張を宣言できる。このような拡張の内部には、修飾子なしでメンバーにアクセスできる複数の暗黙的なレシーバーがある。拡張が宣言されたクラスのインスタンスをディスパッチレシーバー(dispatch receiver)と呼び、拡張メソッドのレシーバー型のインスタンスを拡張レシーバー(extension 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()  // エラー、'Connection'の外部では拡張関数を使用できない。
}

Output:

kotl.in:443

ディスパッチレシーバーと拡張レシーバーの間で名前が衝突する場合、拡張レシーバーが優先される。ディスパッチレシーバーのメンバーを参照するには、修飾された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("Base extension function in BaseCaller")
    }

    open fun Derived.printFunctionInfo() {
        println("Derived extension function in BaseCaller")
    }

    fun call(b: Base) {
        b.printFunctionInfo()   // call the extension function
    }
}

class DerivedCaller : BaseCaller() {
    override fun Base.printFunctionInfo() {
        println("Base extension function in DerivedCaller")
    }

    override fun Derived.printFunctionInfo() {
        println("Derived extension function in DerivedCaller")
    }
}

fun main() {
    BaseCaller().call(Base())   // "Base extension function in BaseCaller"
    DerivedCaller().call(Base())  // "Base extension function in DerivedCaller" - dispatch receiver is resolved virtually
    DerivedCaller().call(Derived())  // "Base extension function in DerivedCaller" - extension receiver is resolved statically
}

Output:

Base extension function in BaseCaller
Base extension function in DerivedCaller
Base extension function in DerivedCaller

可視性に関する注意(Note on visibility)

拡張は、同じスコープで宣言された通常の関数と同じアクセス修飾子を利用する。たとえば:

  • ファイルのトップレベルで宣言されたprivateな拡張は、同じファイル内の他のトップレベル宣言にアクセスできる。
  • 拡張がレシーバー型の外部で宣言されている場合、レシーバーのprivateまたはprotectedメンバーにはアクセスできない。

参考