Kotlin 拡張関数(Extension functions)
概要
Kotlinは、クラスを継承したりDecoratorのようなデザインパターンを使ったりしなくても、クラスを新しい機能で拡張できる機能を提供している。これは拡張(extension)宣言によって行う。
たとえば、変更できないサードパーティライブラリ(Third-party library)のクラスに新しい関数を書くことができる。このような関数は、元のクラスのメソッドであるかのように通常の方法で呼び出せる。この仕組みを拡張関数(Extension functions)という。既存クラスに新しいプロパティを定義できる拡張プロパティ(Properties)もある。
また、既に存在する基本クラスにも関数を追加できる。たとえば、次のようにInt、Stringのような基本型(クラス)にも関数を追加できるという意味である。
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メンバーにはアクセスできない。