[Kotlin]レシーバー指定ラムダとは何か

kotlin-1.0

まえがき

Kotlinのスコープ関数便利ー。

let apply run with also

kotlin-Standard.ktより

ただ本当に使いこなすに ラムダレシーバー指定ラムダ を理解しなければなりません。

// ラムダ
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
// レシーバー指定ラムダ
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

block: (T) -> R)block: T.() -> Unit の違いわかりますか

これを理解としないとblock内の this の罠に引っかかる

サンプル

理解するためのサンプルコードを書きました

class Sample {
    val lambda: (String) -> Unit = {
        Log.d("test", this.javaClass.simpleName) //Sample
        Log.d("test", it) //test
    }

    val receiverLambda: String.() -> Unit = {
        Log.d("test", this.javaClass.simpleName) //String
        Log.d("test", this) //test
    }

}

thisの値が違いますね。なぜこういう動きになるのかKotlin BytecodeをDecompileにして、おってみましょう。

Kotlin BytecodeからDecompile

Android Studioをもっていれば簡単にKotlin BytecodeをDecompileできます。

スクリーンショット 2017-06-30 17.01.13

スクリーンショット_2017-06-30_16_53_59

Decompileしたものがこちら。

@Metadata(
   mv = {1, 1, 6},
   bv = {1, 0, 1},
   k = 1,
   d1 = {"\u0000$\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\u0010\u000e\n\u0002\u0010\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002R\u001d\u0010\u0003\u001a\u000e\u0012\u0004\u0012\u00020\u0005\u0012\u0004\u0012\u00020\u00060\u0004¢\u0006\b\n\u0000\u001a\u0004\b\u0007\u0010\bR\"\u0010\t\u001a\u0013\u0012\u0004\u0012\u00020\u0005\u0012\u0004\u0012\u00020\u00060\u0004¢\u0006\u0002\b\n¢\u0006\b\n\u0000\u001a\u0004\b\u000b\u0010\b¨\u0006\f"},
   d2 = {"Lcom/kamedon/devio2017/validation/Sample;", "", "()V", "lambda", "Lkotlin/Function1;", "", "", "getLambda", "()Lkotlin/jvm/functions/Function1;", "receiverLambda", "Lkotlin/ExtensionFunctionType;", "getReceiverLambda", "production sources for module app"}
)
public final class Sample {
   @NotNull
   private final Function1 lambda = (Function1)(new Function1() {
      // $FF: synthetic method
      // $FF: bridge method
      public Object invoke(Object var1) {
         this.invoke((String)var1);
         return Unit.INSTANCE;
      }

      public final void invoke(@NotNull String it) {
         Intrinsics.checkParameterIsNotNull(it, "it");
         Log.d("test", Sample.this.getClass().getSimpleName());
         Log.d("test", it);
      }
   });
   @NotNull
   private final Function1 receiverLambda;

   @NotNull
   public final Function1 getLambda() {
      return this.lambda;
   }

   @NotNull
   public final Function1 getReceiverLambda() {
      return this.receiverLambda;
   }

   public Sample() {
      this.receiverLambda = (Function1)null.INSTANCE;
   }
}

ラムダ

   private final Function1 lambda = (Function1)(new Function1() {
      // $FF: synthetic method
      // $FF: bridge method
      public Object invoke(Object var1) {
         this.invoke((String)var1);
         return Unit.INSTANCE;
      }

      public final void invoke(@NotNull String it) {
         Intrinsics.checkParameterIsNotNull(it, "it");
         Log.d("test", Sample.this.getClass().getSimpleName());
         Log.d("test", it);
      }
   });

ラムダは、Function1というインターフェースがつくられ、invokeという関数ができます。itという名のStringを引数なっていますね。

thisはFunction1インターフェースを実装した場所すなわち、Sampleになります。

レシーバー指定ラムダ

   d2 = {"Lcom/kamedon/devio2017/validation/Sample;", "", "()V", "lambda", "Lkotlin/Function1;", "", "", "getLambda", "()Lkotlin/jvm/functions/Function1;", "receiverLambda", "Lkotlin/ExtensionFunctionType;", "getReceiverLambda", "production sources for module app"}

ExtensionFunctionTypeつまり、拡張関数です。この場合はStringにgetReceiverLambdaというメソッドを作っていますね このFunction1の定義場所はStringなのでthisはStringになります。

こんなこともできちゃいます。

class Sample {

    //この関数内で使用できる拡張関数。Stringにlog()を拡張
   fun log(log: String.() -> Unit) {
        "test".log()
        "hoge".log()
        "foo".log()
    }

    fun showLog() {
        log { print(this) }
        log { Log.d("hoge", this) }
    }
}

fun log()内だけで有効なString拡張関数をいれるイメージです。これを利用するとKotlinっぽいDSLが作りやすくなりますので、何度サンプルを書いて試してみることをおすすめします。

まとめ

ラムダとレシーバー指定ラムダはなんとなくイメージできましたか?

レシーバーって結局なんだ?っと思った方いるかもしれません。それは次回の話のネタに。

  • yy_yank

    >ExtensionFunctionTypeつまり、拡張関数です。
    >StringにgetReceiverLambdaというメソッドを作っていますね

    ExtensionFunctionTypeは拡張関数のためのマーカーインターフェースのようなので、
    拡張関数を意味するメタデータというのは正しいと思います。
    しかし、おそらく拡張関数として扱われているのは
    receiverLambdaそのもので、getReceiverLambdaではないのではないでしょうか(getReceiverLambda自体はただのgetterなので)。
    また、なんとなく文章の意図は分かるのですが、
    Stringに対してメソッドを作るような動きはしませんし出来ません。
    あくまで「レシーバーがStringなのでその関数スコープでのthisに関してもString型になる」ということだと思います。

    実際に、サンプルコードのreceiverLambdaはjavapすると
    Sample$receiverLambda$1
    のようになると思います。

    >このFunction1の定義場所はStringなのでthisはStringになります。

    定義場所というのがよく分からないですが、
    ブログのサンプルコードの
    ・lambda
    ・receiverLambda
    に関しては定義場所はSampleクラス内だと思います。
    Function1の定義場所はkotlin.jvm.functionsパッケージでKotlinのランタイムjarが持っているものですよね。

    おそらく定義場所というより、レシーバーそのもののことを定義場所と表現したのでしょうか。
    レシーバーがStringなので引数として渡しているreceiverがStringになるということかと思います。
    receiverに関しては次のブログ記事が詳しいです。
    http://lvla.hatenablog.com/entry/2016/12/14/020741

    • 亀井栄利

      ご指摘ありがとうございます。
      「定義場所」は不適切でした。
      > おそらく定義場所というより、レシーバーそのもののことを定義場所と表現したのでしょうか。

      thisはレシーバーのことを指しています。
      定義場所をレシーバーのことを言おうとしていました。

      わかりやすい参考文献もありがとうございます!
      レシーバー指定ラムダは、Javaから入ってくるとつまづくところだと思い、
      わかりやすい表現したつもりが、逆にややこしくしてしまいました。
      ご指摘していただきありがとうございます!