DroidKaigi2019で、リクルートさんのブースで出されていた問題が面白かったので、なぜそうなるのか調べてみたので、解説したいと思います。
問題
val x = object: ((Int) -> Unit) -> Unit by {} {} {}
と宣言した時、Xの型は?
- Int
- Unit
- (Int) -> Unit
- ((Int) -> Unit) -> Unit
答えは解説の後に書きます、皆さんも一度考えてみてください。
解説
val x = object: ((Int) -> Unit) -> Unit by {} {} {}
では、上記のコードがどうなっているのか解説していきたいと思います。
objectキーワード
まず、objectキーワードですが、以下の様に無名クラスを作成することが出来ます。
この無名クラスはシングルトンになってます。
val a = object: OnClickListener {
// オブジェクトのブロック クラスと同じ様にメンバー変数やメソッドの定義が出来る
override fun onClick(v: View) {
// ...
}
}
val x = object: ((Int) -> Unit) -> Unit
上記のコードは、((Int) -> Unit) -> Unit
を継承した無名クラスを作っているということになります。
この無名クラスは((Int) -> Unit) -> Unit
を継承しているのでinvoke(p1: (Int) -> Unit)
をオーバーライドする必要があります。
val x = object: ((Int) -> Unit) -> Unit {
override fun invoke(p1: (Int) -> Unit) {
// ...
}
}
問題のコードでは、なぜinvoke
メソッドをオーバライドせずに動くのかは、次の委譲(Delegation)で説明します。
byキーワード 委譲(Delegation)
Kotlinの委譲には2種類あり、今回はクラスデリゲーションが使われています。
- クラスデリゲーション
- プロパティデリゲーション
クラスデリゲーションとは?
継承しているインターフェイスのメソッドを、同じシグネチャのメソッドを実装しているインスタンスに委譲できる仕組みです。
interface ClickListener {
fun onClick()
}
object DefaultClickListener : ClickListener {
override fun onClick() {
println("DefaultClickListener onClick")
}
}
object HogeButton: ClickListener by DefaultClickListener
fun main(args: Array<String>) {
HogeButton.onClick() // "DefaultClickListener onClick"
}
invoke
メソッドをオーバライドせずに動いたの理由は、((Int) -> Unit) -> Unit
のラムダからinvoke
メソッドを委譲していたからです。
あとはobjectのボディを書くとobject式は完成です。
val x = object: ((Int) -> Unit) -> Unit by { /* ((Int) -> Unit) -> Unit型のラムダ */ } { /* objectのボディ */ } {}
では、byの後にある三つ目の{}
は、何なのかをみていきましょう
対応する引数として渡されるラムダ式は括弧の外側に置くことができる
byの後にある三つ目の{}
の正体ですが、これは object: ((Int) -> Unit) -> Unit by {} {}
のメソッド呼び出しです。
object: ((Int) -> Unit) -> Unit by {} {}
を実行するにはinvoke(p1: (Int) -> Unit)
を呼び出す必要があります。
val o = object: ((Int) -> Unit) -> Unit by {} {}
o.invoke({ i -> /* Unit */ })
o({ i -> /* Unit */ }) // invokeは()に省略可能です
o { i -> /* Unit */ } // 引数の最後のラムダは丸括弧の外に記述でき、それ以外に引数がない場合は丸括弧を省略できます。
このinvoke
の省略と、最後のラムダの場合は丸括弧の外で記述できる記法を使い
object: ((Int) -> Unit) -> Unit by {} {}
を実行していたということですね。
これで問題の謎は全て解けました。
回答 まとめ
整理してみると以下の様になります。
val x = object: ((Int) -> Unit) -> Unit by { /* ((Int) -> Unit) -> Unit型のラムダ */ } { /* objectのボディ */ } { /* object: ((Int) -> Unit) -> Unit by {} {} の実行、呼び出し */ }
byの後にある1つ目の{}
を実行しているので、回答は2. Unit
になります。
Kotlinでは、様々な省略記法などがあり様々な書き方が出来ます。
今回の問題の様なコードをプロダクトで書くことはないと思いますが、難読なコードが書けてしまうので気をつけて使いましょう。