LoginSignup
45
28

More than 5 years have passed since last update.

Kotlinの"fun Foo.bar(function: Foo.() -> Unit)"について理解したい

Last updated at Posted at 2017-07-18

きっかけ

Google I/OのKotlinの動画で以下のようなコードが登場します。
inline fun SQLiteDatabase.transaction(body: SQLiteDatabase.() -> Unit)

image.png

Kotlinに慣れている方などは普通に読める方は読めると思いますが
自分はちょっとうん?ってなりました


Kotlinでよくわからない事があるときの対応法

もちろんKotlinのドキュメントを見たりします。
そのままで理解できるのが一番いいですが、理解しにくいときは、もともとJavaをやっていた方は、Javaで考えれば何となく分かるという事があると思います。
コードを開いた状態で、Tools->Kotlin->Show Kotlin BytecodeでDecompileボタンを押すと一度バイトコードになった後にJavaに変換してみれるので、Javaのコードでどうなっているのか見ることができます。

image.png


文法の名前がわからない時

文法の名前がわからない時などPsiViewerというIntelliJプラグインがあるのでそれを使うとどういうパースがされているのかAST(抽象構文木)を見ることが出来ます。(普通はIntelliJプラグインを作る時に利用するプラグインです。)
例えばこの例ではinline fun Hoge.f(body: Hoge.() -> Unit)Hoge.() -> Unitの部分がFUNCTION_TYPE_RECEIVERということがわかります。FUNCTION TYPE RECEIVER kotlinでググったりしていれば何なのかをつかむことができます。
image.png


まずは普通のメソッドはどうなるか

ただの呼び出しがそのままJavaになった形です。

Kotlin

MainActivity.kt
val hoge = Hoge()
hoge.fuga()
Hoge.kt
class Hoge {
    fun fuga(){
    }
}

Javaへのデコンパイル

Hoge hoge = new Hoge();
hoge.fuga();
public final class Hoge {
   public final void fuga() {
   }
}

拡張関数(extension function)

拡張関数を使うと既存のクラスに外からメソッドを生やすことができます。

Kotlin

以下のような拡張関数(extension function)が書いてあるFoo.ktを作ってみます。
この例ではHogeクラスにメソッドbar()を生やしています。

Foo.kt
fun Hoge.bar() : Unit{
    // thisがHogeクラスかのように
    // Hogeのメソッドが呼び出せる
    fuga()
}

そして以下のように呼び出すことができます。

MainActivity.kt
val hoge = Hoge()
hoge.bar()

Hogeクラスは一つ前の例のままです。

Hoge.kt
class Hoge {
    fun fuga(){
    }
}

Javaへのデコンパイル

FooKtクラスにbarというstaticメソッド(Kotlinだとトップレベル関数)が作られ、それを呼び出す形に変換されます。

Hoge hoge = new Hoge();
FooKt.bar(hoge);

\$receiverという引数でHogeが渡されてきました。
fuga()の呼び出しは、\$receiverへの呼び出しへと変わっています。

public final class FooKt {
   public static final void bar(@NotNull Hoge $receiver) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      $receiver.fuga();
   }
}

関数型(function type)を引数で渡す

function literal with receiverを見る前に普通のfunction typeを渡す場合を見ておきましょう。
function typeを引数で渡すことができます。

Kotlin

MainActivity.kt
val hoge = Hoge()
// ラムダが渡せる
// hoge.fuga{println()}のように()は省略可
hoge.fuga({println()}) 
Hoge.kt
class Hoge {
    fun fuga(function: () -> Unit) {
        function()
    }
}

Javaへのデコンパイル

以下のようになります。
ラムダのインスタンスのMainActivity\$onCreate\$2.INSTANCEを引数にhoge.fugaを呼び出します。

hoge.fuga(MainActivity$onCreate$2.INSTANCE);

Hogeクラスはそのままという感じですね。

public final class Hoge {
   public final void fuga(@NotNull Function0 function) {
      Intrinsics.checkParameterIsNotNull(function, "function");
      function.invoke();
   }
}

MainActivity\$onCreate\$2ではFunction0を実装し、println();を行います。

class MainActivity$onCreate$2 extendsLambda implements Function0 {
    public static final MainActivity$onCreate$2 INSTANCE 
        = new MainActivity$onCreate$2();
    public void invoke(){
        println();
    }
}

コードの見方の詳細


Android Studio内のデコンパイルではうまく変換されませんでした。

hoge.fuga((Function0)null.INSTANCE);

なので手動でバイトコードを見ることにします。

普通にクラス(MainActivity\$onCreate\$2)が生成されて、そこにINSTANCEという名前でstatic finalなフィールドにインスタンスが保持され、MainActivity\$onCreate\$2クラスのinvoke()メソッドに実装が作られるみたいです。

javap -c ./app/build/tmp/kotlin-classes/debug/com/.../MainActivity.class

53: getstatic     #58 // Field com/.../MainActivity$onCreate$2.INSTANCE:Lcom/.../MainActivity$onCreate$2;
...
kotlin/jvm/functions/Function0
59: invokevirtual #64 // Method com/.../Hoge.fuga:(Lkotlin/jvm/functions/Function0;)V
62: return

つまりこんなイメージです

hoge.fuga(MainActivity$onCreate$2.INSTANCE);

MainActivity\$onCreate\$2が気になるので、それのバイトコードを見てみます。

javap -c ./app/build/tmp/kotlin-classes/debug/com/.../MainActivity\$onCreate\$2.class

final class com.....MainActivity$onCreate$2 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<kotlin.Unit> {
  public static final com.....MainActivity$onCreate$2 INSTANCE;
....
  public final void invoke();
    Code:
 0: getstatic     #24 // Field java/lang/System.out:Ljava/io/PrintStream;
 3: invokevirtual #29 // Method java/io/PrintStream.println:()V
 6: return
class MainActivity$onCreate$2 extendsLambda implements Function0 {
    public static final MainActivity$onCreate$2 INSTANCE 
        = new MainActivity$onCreate$2();
    public void invoke(){
        println()
    }
}
public final class Hoge {
   public final void fuga(@NotNull Function0 function) {
      Intrinsics.checkParameterIsNotNull(function, "function");
   }
}


fun f(body: Hoge.() -> Unit) (function literal with receiver)を試す

言語的な詳細はここにあるようです
https://kotlinlang.org/docs/reference/lambdas.html

Kotlin

これはfunction literal with receiverといい、使うと、引数に渡すラムダ内でthisが指定されたクラスの中のようにメソッドを呼び出すことができます。
つまり以下のラムダの中ではPiyoのクラスのメソッド内のような動きをします。
例:
image.png

普通は以下のように利用します。

MainActivity.kt
val hoge = Hoge()
// ラムダ内で、thisがPiyoかのように
// Piyo#hogera()メソッドを呼び出せる
hoge.fuga({ hogera()}) 
Hoge.kt
class Hoge {
    fun fuga(function: Piyo.() -> Unit) {
        // ここで引数を渡さないとコンパイルエラー
        function(Piyo()) 
    }
}
Piyo.kt
class Piyo {
    fun hogera(){
    }
}

Javaへのデコンパイル

MainActivity\$onCreate$2.INSTANCEを取得してhoge.fuga()を呼び出します。

hoge.fuga(MainActivity$onCreate$2.INSTANCE)

引数にpiyoがあるので、それを使って呼び出しています。thisがうまく引数のpiyoに切り替わっているようです。

class MainActivity$onCreate$2 extends Lambda implements Function1 {
    public static final MainActivity$onCreate$2 INSTANCE 
        = new MainActivity$onCreate$2();
    public void invoke(Piyo piyo){
        piyo.hogera()
    }
}

Hogeクラスはそのままという感じのようです。

public final class Hoge {
   public final void fuga(@NotNull Function1 function) {
      Intrinsics.checkParameterIsNotNull(function, "function");
      function.invoke(new Piyo());
   }
}

コードの見方の詳細


一個前と同じようにAndroid Studio上では見れないのでバイトコードで見ていきます

MainActivity\$onCreate\$2を取得して、hoge.fuga()メソッドを呼び出します。

      51: astore_2
      52: aload_2
      53: getstatic     #58                 // Field com/github/takahirom/kotlinsupportprojectpreview6/MainActivity$onCreate$2.INSTANCE:Lcom/github/takahirom/kotlinsupportprojectpreview6/MainActivity$onCreate$2;
      56: checkcast     #60                 // class kotlin/jvm/functions/Function1
      59: invokevirtual #64                 // Method com/github/takahirom/kotlinsupportprojectpreview6/Hoge.fuga:(Lkotlin/jvm/functions/Function1;)V

つまり以下のようになります。

hoge.fuga(MainActivity$onCreate$2.INSTANCE)

MainActivity\$onCreate\$2は以下のように普通にメソッドとして呼び出しを行うように変換が行われるようです。

final class com.....MainActivity$onCreate$2 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function1<com.....Piyo, kotlin.Unit> {
  public static final com.....MainActivity$onCreate$2 INSTANCE;
...
  public final void invoke(com.....Piyo);
    Code:
...
 7: invokevirtual #33 // Method com/.../Piyo.hogera:()V
10: return

Javaにすると以下のようになります。

class MainActivity$onCreate$2 extendsLambda implements Function1 {
    public void invoke(Piyo piyo){
        piyo.hogera()
    }
}

Hogeクラスは以下のようになります。

public final class Hoge {
   public final void fuga(@NotNull Function1 function) {
      Intrinsics.checkParameterIsNotNull(function, "function");
      function.invoke(new Piyo());
   }
}



fun Hoge.f(body: Hoge.() -> Unit) (拡張関数 + function literal with receiver)

これがタイトルで行いたかったものとなります。
これまでの拡張関数(Extension)とfunction literal with receiverを組み合わせた形になります。

Kotlin

MainActivity.kt
val piyo = Piyo()
// piyoのインスタンスにbarが生えている
piyo.bar { 
    // インスタンスの指定がなくてもPiyo#hogera()メソッドにアクセスできる
    hogera() 
}
Foo.kt
fun Piyo.bar(function: Piyo.() -> Unit) : Unit{
    function() // 引数がなくてもエラーにならない
}
Piyo.kt
class Piyo {
    fun hogera(){
    }
}

Javaへのデコンパイル

拡張関数なので、FooKtへの呼び出しになり、引数にpiyoが渡ります。

FooKt.bar(piyo, MainActivity$onCreate$2.INSTANCE);

渡されたpiyoを使ってfunctionのinvoke()を呼び出します。invoke()を呼び出す時にKotlinのコードでは引数を渡していませんでしたが、追加され、呼び出されました。

public final class FooKt {
   public static final void bar(Piyo $receiver, Function1 function) {
...
      // 引数が追加されている!
      function.invoke($receiver);
   }
}

引数にpiyoがあるので、それを使って呼び出しています。thisがうまく引数のpiyoに切り替わっているようです。

class MainActivity$onCreate$2 implements Function1 {
    public static final MainActivity$onCreate$2 INSTANCE 
        = new MainActivity$onCreate$2();
    public void invoke(Piyo piyo) {
        piyo.hogera();
    }
}

詳細


getstaticでMainActivity\$onCreate\$2.INSTANCEを取得します
FooKt.barを取得したINSTANCEを引数に呼び出します。

51: astore_2
52: aload_2
53: getstatic     #58 // Field com/.../MainActivity$onCreate$2.INSTANCE:Lcom/.../MainActivity$onCreate$2;
56: checkcast     #60 // class kotlin/jvm/functions/Function1
59: invokestatic  #66 // Method com/.../FooKt.bar:(Lcom/.../Piyo;Lkotlin/jvm/functions/Function1;)V

つまり以下のようなイメージになります。

FooKt.bar(piyo, MainActivity$onCreate$2.INSTANCE);

FooKtでは以下のようにfunctionに対して呼び出しを行います。
ここで特殊なことは、function()と書いていたのが、第一引数のPiyoが勝手にfunction()の引数に追加されています。

public final class FooKt {
   public static final void bar(@NonNull Piyo $receiver,@NonNull  Function1 function) {
...
      // 引数が追加されている!
      function.invoke($receiver);
   }
}

MainActivity\$onCreate\$2のinvokeメソッドは以下のようになり、Piyoが渡されて、それにメソッドを呼び出す形になります。

  public final void invoke(com.....Piyo);
    Code:
...
 7: invokevirtual #33 // Method com/.../Piyo.hogera:()V
10: return

つまり以下のような形です。

class MainActivity$onCreate$2 implements Function1 {
    public void invoke(Piyo piyo) {
        piyo.hogera();
    }
}


まとめ

  • Javaへのデコンパイルでなんとなく分かった気がする
(いいか悪いかは別として脳内でJavaに変換できそう)
  • Kotlinでわからない時はデコンパイルして見てみると良いかもしれない
  • 拡張関数もfunction literal with receiverもほぼ同じで、
引数にreceiverとして渡されたものをthisとして使えるように変換してくれる形で実装されている。
  • 拡張関数とfunction literal with receiverを組み合わせると呼び出す時に勝手に引数を追加してくれる形で実装されている

おまけ

有名な話ですが、Kotlinのスコープ関数も同じ仕組みで実装されています。
ここが個人的にKotlinで好きなところです。
今回のでJavaとして読めるはず、、

fun <R> run(block: () -> R): R = block()
fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
fun <T, R> T.let(block: (T) -> R): R = block(this)
45
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
28