Kotlin で拡張関数をオーバーライドして実装を切り替えられるぞ!
背景 : コルーチンの Select 式の実装を理解するのが難しかった
コルーチンのドキュメントを読んでいて select
関数というのが出てきたのだけど、これの実装がどうなっているのかすぐにはわからなかった。
suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) { select<Unit> { // <Unit> means that this select expression does not produce any result fizz.onReceive { value -> // this is the first select clause println("fizz -> '$value'") } buzz.onReceive { value -> // this is the second select clause println("buzz -> '$value'") } } }kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub
fizz.onReceive
は SelectClause1<E>
型のプロパティなのだけど、その後ろのラムダが何なのかぱっと見はわからなかったのである。 (SelectClause1<E>
型には invoke
は定義されていない。)
select
関数のシグネチャは以下。 引数の関数型はレシーバ付きで、レシーバの型は SelectBuilder
である。
public inline suspend fun <R> select(crossinline builder: SelectBuilder<R>.() -> Unit): R
この SelectBuilder
を見ると、下記のような拡張関数が定義されていた。 これらが fizz.onReceive { /* ... */ }
の実体なのであった。
public operator fun <Q> SelectClause1<Q>.invoke(block: suspend (Q) -> R)
しかも SelectBuilder
はインターフェイスで、拡張関数は別のクラスで実装されていた。
学び
拡張関数をオーバーライドできる
Extensions declared as members can be declared as
Extensions - Kotlin Programming Languageopen
and overridden in subclasses.
拡張関数をメンバーとして定義できるのは知ってたのだけど、オーバーライドできるとは知らなかった!
interface StringExtensionScope { fun String.bar(): String } class StringExtensionScopeImpl : StringExtensionScope { override fun String.bar() = this + " bar" }
そして実行時に実装を変更できる
拡張関数を定義した型をレシーバとするレシーバ付きの関数型を使うと、ラムダ内をスコープにして拡張関数を有効にできる! 実行時に実装を変更することも可能!
val printFooBar: StringExtensionScope.() -> Unit = { // StringExtensionScope で定義されている拡張関数を利用できる。 println("foo".bar()) } // そして実行時に拡張関数の実装を変更できる。 printFooBar(StringExtensionScopeImpl())
拡張関数といえば 「静的に解決されるものである」 という印象だったのでオーバーライドして実行時に実装を切り替えられるとは思っていなかったけど実際は切り替えることができる。 ちなみに拡張レシーバ (extension receiver) の型はやはり静的に見られるので、拡張レシーバの型に応じて動的に実装を切り替えるということはできない。
This means that the dispatch of such functions is virtual with regard to the dispatch receiver type, but static with regard to the extension receiver type.
Extensions - Kotlin Programming Language
終わり
小ネタだけど、拡張関数は常に静的に解決されるものだと思ってたのでちょっとびっくりした。
Kotlin も拡張関数と operator とレシーバ付き関数型が組み合わさってくると結構コード追いづらくなるなーと感じる。 (まあ Scala とかと比べるとまだまだ追いやすい方だとは思うけど。)