ザクっと理解するKotlin Extensions(拡張)
Kotlinの公式文章をもとに、拡張関数や拡張プロパティなどのExtension(拡張)についてまとめました。
拡張の書き方や実行される際の注意点、null許容レシーバーの説明もわかりやすくしていましう。
正直、最後の方は理解するのも大変でしたし、自分で開発する際にはややこしくなる可能性が高いので使わないかもしれません。
しかし他の人が使う可能性やAPIで使われる可能性はあるので、学んで損はないと思います。
拡張とは
Kotlinではクラスからの継承やDecoratorなどのデザインパターンを使用せずとも、クラスや関数の機能を拡張することができます。
この機能を使えば、編集が不可能なサードパーティ製ライブラリのクラスやインターフェースに新しい関数を加えるなどができます。
追加した関数は普通の関数呼び出しと同じ呼び出し方で使用できます。
これを拡張関数と呼び、他にも既存クラスに新しくプロパティを宣言できる拡張プロパティもあります。
Extension Functions(拡張関数)
拡張関数を宣言するには関数名の前に、拡張する型を表すレシーバータイプを書きます。
fun String.repeat(times: Int) { ... }
この例ではレシーバータイプとしてStringを指定しているため、String型が拡張されます。
fun main() {
"Hello".repeat(3)
}
fun String.repeat(times: Int) {
var x = 1
while (x <= times) {
println(this)
x++
}
}
このようにString型でrepeat拡張関数が呼び出せるようになります。
repeat拡張関数内のthisは呼び出しているレシーバーオブジェクトを指します。
この例の場合、”Hello”がrepeat拡張関数を呼び出しているためthisは”Hello”を意味します。
拡張は静的解決
拡張を宣言した際、実際のクラスに変更を加えるわけではなく、レシーバーオブジェクトとして指定した型を持つ変数でドットを使うとその関数の呼び出しができるだけです。
拡張は静的に解決され実行されるため、実行時の型によって呼び出す拡張関数が決まるのではなく、拡張関数を呼び出している式の型により決まります。
fun main() {
val class01 = Class01()
printName(class01) // 結果: Class01
val class02 = Class02()
printName(class02) // 結果: Class01
}
open class Class01
fun Class01.getName() = "Class01"
class Class02: Class01()
fun Class02.getName() = "Class02"
fun printName(obj: Class01) {
println(obj.getName())
}
class01と02はそれぞれ拡張関数を持っており、02は01を継承しています。
printName()関数で拡張関数を呼び出していますが、01でも02でも”Class01″と表示されます。
これはprintName()関数でClass01型のobjに対してgetName()拡張関数を呼び出しているからです。
クラスが持っている関数名と拡張関数の名前が同じかつパラメータも同じ場合、常にクラスのメンバー(クラスが持っている関数)が実行されます。
fun main() {
val class01 = Class01()
println(class01.getName()) // 結果: Class01
}
class Class01 {
fun getName() = "Class01"
}
fun Class01.getName() = "Extension function"
しかしパラメータが違う場合は問題なく実行されます。
fun main() {
val class01 = Class01()
println(class01.getName()) // 結果: Class01
println(class01.getName("in Class01")) // 結果: Extension function in Class01
}
class Class01 {
fun getName() = "Class01"
}
fun Class01.getName(text: String) = "Extension function ${text}"
null許容レシーバー
拡張関数はnull許容のレシーバータイプとでも宣言できます。
変数がnullの場合でも拡張関数を呼ぶことができ、ボディ内で this == null でnullチェックができます。
fun main() {
"Hello".repeat(3) // 結果: Hello Hello Hello
null.repeat(5) // 結果:
}
fun String?.repeat(times: Int) {
if (this == null) return
var x = 1
while (x <= times) {
println(this)
x++
}
}
拡張プロパティ
Kotlinでは拡張プロパティをサポートしています。
fun main() {
val list = listOf("abc", "def")
println(list.lastIndex) // 結果: 1
}
val <T> List<T>.lastIndex: Int
get() = size - 1
拡張プロパティは実際にクラスの中で宣言されるわけではないため、バッキングフィールドを持つことができません。
そのためgetterとsetterを指定する必要があり、初期化をしようとするとエラーになります。
Companion object(コンパニオンオブジェクト)
クラス内にコンパニオンオブジェクトがある場合、コンパニオンオブジェクトに対しても拡張関数や拡張プロパティを設定できます。
「クラス名.コンパニオンオブジェクトの拡張関数名」で呼び出せます。
class Class01 {
companion object {}
}
fun Class01.Companion.getName() = "Companion in Class01"
fun main() {
println(Class01.getName())
}
拡張のスコープ
多くの場合、拡張をパッケージの直下であるトップレベルで宣言できます。
package org.example.declarations
fun List<String>.getLongestString() { /*...*/}
その拡張をそのパッケージ外で使うには、importする必要があります。
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}
拡張をメンバーとして宣言する
あるクラスAに対する拡張を他のクラスBの中で宣言することもできます。(8行目)
この拡張の中では暗黙的なレシーバーが使われるため、修飾子を使用せずにメンバーにアクセスできます。(8, 9行目)
拡張を宣言しているクラスインスタンスのことをディスパッチャーレシーバーと呼び、拡張関数が呼ばれるレシーバータイプのインスタンスのことを拡張レシーバーと呼びます。
下記コードの場合、ディスパッチャーレシーバーはClass02、拡張レシーバーはClass01です。
class Class01 {
fun printClass01Name() = println("Class01")
}
class Class02 {
fun printClass02Name() = println("Class02")
fun Class01.printClassInfo() { // Class02の中でClass01の拡張関数を宣言
printClass01Name() // Class01のprintClass01Nameを呼び出し
printClass02Name() // Class02のprintClass02Nameを呼び出し
}
fun printInfo() {
Class01().printClassInfo() // Class02の中でClass01の拡張関数を呼び出し
}
}
fun main() {
Class02().printInfo() // 結果: Class01 Class02
// Class01().printClassInfo() // printClassInfo()はClass02内でのみ参照可能です
}
ディスパッチャーレシーバーと拡張レシーバーのメンバーの名前が同じ場合、拡張レシーバーが優先されます。
thisを使うとディスパッチャーレシーバーのメンバーを参照できます。(12行目)
class Class01 {
fun printClass01Name() = println("Class01")
}
class Class02 {
fun printClass02Name() = println("Class02")
fun Class01.printClassInfo() {
printClass01Name()
printClass02Name()
println(toString())
println(this@Class02.toString())
}
fun printInfo() {
Class01().printClassInfo()
}
}
fun main() {
Class02().printInfo()
}
メンバーとして宣言された拡張はopenをつけることでサブクラスでオーバーライドできます。
open class Base
class Class01: Base()
open class BaseCaller {
open fun Base.printFunctionInfo() {
println("Base extension function in BaseCaller")
}
open fun Class01.printFunctionInfo() {
println("Derived extension function in BaseCaller")
}
fun call(b: Base) {
b.printFunctionInfo()
}
}
class Class01Caller: BaseCaller() {
override fun Base.printFunctionInfo() {
println("Base extension function in Class01Caller")
}
override fun Class01.printFunctionInfo() {
println("Derived extension function in Class01Caller")
}
}
fun main() {
BaseCaller().call(Base())
Class01Caller().call(Base())
Class01Caller().call(Class01())
}
30行目: BaseCaller()でBaseCallerをインスタンス化しBaseとClass01の拡張関数を設定
30行目: BaseCaller().call(Base())でcallにBaseクラスを渡し実行すると、BaseCaller()のインスタンス化時にBaseに対して設定されたprintFunctionInfo()拡張関数が実行される
→Base extension function in BaseCallerを表示
31行目: Class01Caller()でClass01Callerをインスタンス化しBaseとClass01の拡張関数を設定
31行目: Class01Caller().call(Base())でcallにBaseクラスを渡し実行すると、Class01Caller()のインスタンス化時にBaseに対して設定されたprintFunctionInfo()拡張関数が実行される
→Base extension function in Class01Callerを表示
32行目: Class01Caller()でClass01Callerをインスタンス化しBaseとClass01の拡張関数を設定
32行目: Class01Caller().call(Class01())でcallにClass01クラスを渡し実行すると、Class01Caller()のインスタンス化時にBaseに対して設定されたprintFunctionInfo()拡張関数が実行される
→Base extension function in Class01Callerを表示
これはBaseCallerクラスのcall関数のパラメータの型がBaseになっていること、拡張は静的に解決され実行されることを考えると、callに対してClass01を渡してもBaseのprintFunctionInfo()が実行されます。
しかしprintFunctionInfo()はopenで宣言されているため、Class01Callerをインスタンス化する際に上書きはできます。
よって31と32行目は同じ結果が出力されます。
別の言い方をすると、ディスパッチャーレシーバー
このような関数のディスパッチがディスパッチレシーバの型に対しては仮想的であり、拡張レシーバの型に対しては静的であることを意味します。