Kotlin文法におけるラムダ式の完全な分析について(6)

簡単な説明:本日発表されたKotlinトークシリーズの第6弾では、Kotlinのラムダ式について説明します。ラムダ式はなじみのないものであってはなりません。Java8で導入された非常に重要な機能により、開発者は元の面倒な文法から解放されますが、残念ながらJava8バージョンしか使用できません。Kotlinはこの問題を補います。Kotlinでのラムダ式とJavaの混合プログラミングは、Java8より前のバージョンをサポートできます。Kotlinのラムダ式を確認するために、次の質問をしてみましょう。

  • 1. Kotlinのラムダ式を使用する理由(理由)
  • 2. Kotlinのラムダ式の使用方法(方法)?
  • 3.Kotlinのラムダ式はどこで一般的に使用されていますか?
  • 4.スコープ変数とKotlinのラムダ式の変数キャプチャ
  • 5.Kotlinのラムダ式のメンバー参照

1. Kotlinのラムダ式を使用する理由

Kotlinでラムダ式が使用される理由に関する上記の質問には、3つの主な理由があると思います。

  • 1. Kotlinのラムダ式は、より簡潔で理解しやすい構文で関数を実装し、開発者を元の冗長で時間のかかる構文宣言から解放します。関数型プログラミングでフィルタリング、マッピング、変換、およびその他の演算子を使用してコレクションデータを処理できるため、コードを関数型プログラミングのスタイルに近づけることができます。
  • 2. Java 8より前のバージョンはLambda式をサポートしていませんが、KotlinはJava 8より前のバージョンと互換性があり、優れた相互運用性を備えています。これは、Java8より前のバージョンとKotlinの混合開発モデルに非常に適しています。Java8より前のバージョンではラムダ式を使用できないというボトルネックを解決しました。
  • 3. Java 8バージョンでのLambda式の使用は多少制限されており、本当の意味でクロージャをサポートしていませんが、Kotlinのラムダはクロージャの本当のサポートです。(この問題について、なぜ以下で説明します)

2.Kotlinのラムダ式の基本構文

1.ラムダ式の分類

Kotlinでは、ラムダ式は実際には2つのカテゴリに分類できます。1つは通常のラムダ式で、もう1つはレシーバー付きのラムダ式です(関数は非常に強力で、後で特別な分析ブログがあります)。これら2つのラムダの使用シナリオと使用シナリオも大きく異なります。最初に、次の2つのラムダ式の型宣言を見てみましょう。

 

レシーバーを使用したラムダ式は、withの宣言や標準関数の適用など、Kotlinの標準ライブラリ関数でも非常に一般的です。

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

上記のラムダ式の分類を見て、前の拡張関数について考えますか?前の図について考えますか?

 

以前のブログで通常の機能と拡張機能について述べたことと似ていますか?通常のラムダ式は通常の関数に対応する宣言に似ており、レシーバーを使用したラムダ式は対応する拡張関数に似ています。拡張関数は、レシーバータイプを宣言し、レシーバーオブジェクトを使用して、メンバー関数呼び出しと同様に直接呼び出すことです。実際、そのメソッドとプロパティには、レシーバーオブジェクトインスタンスを介して直接アクセスします。

2.ラムダの基本構文

ラムダの標準形式は、基本的に3つの条件が満たされていることを示しています。

実際のパラメータが含まれています

関数本体が含まれています(関数本体は空ですが、宣言する必要があります)

上記は中括弧で囲む必要があります

 

上記はラムダ式の最も標準的な形式です。この標準形式は将来の開発ではあまり見られない可能性があり、より単純化された形式です。以下はラムダ式の単純化ルールの紹介です。

3.ラムダ構文の簡略化された変換

Kotlinの言語はのインテリジェントな推論をサポートしているため、実際のパラメータータイプを省略できるなど、標準のラムダ式の形式はまだ少し冗長であることがわかるため、将来的には、ラムダ式の簡略化バージョンをより頻繁に使用する予定です。コンテキストに基づくタイプなので、冗長な文法を省略して破棄できます。ラムダの単純化ルールは次のとおりです。

 

 

注:文法的な簡略化は両刃の剣です。簡略化は優れていますが、シンプルで使いやすいですが、悪用することはできず、コードの可読性を考慮する必要があります。上記のラムダ削減の最も単純な形式これは、一般的に複数のLambdaがネストされている場合、コードの可読性を著しく損なうため、使用をお勧めしません。結局、開発者でさえ、それが何を意味するのかわからないと推定されます。たとえば、次のコード:

これは、KotlinライブラリのjoinToString拡張関数です。最後のパラメーターは、コレクション要素タイプT受け取り、CharSequenceタイプを返すラムダ式です

//joinToString内部声明
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
    return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}


fun main(args: Array<String>) {
    val num = listOf(1, 2, 3)
    println(num.joinToString(separator = ",", prefix = "<", postfix = ">") {
        return@joinToString "index$it"
    })
}

joinToStringの呼び出しは、ラムダ式をパラメーターとして使用する単純化された形式であり、括弧から取り出されていることがわかります。ラムダ式が適用される場所が表示されないため、呼び出しに少し混乱が生じます。そのため、内部実装に精通していない開発者が理解するのは困難です。この種の問題に対して、Kotlinは実際に解決策を提供します。これは、以前のブログで説明した名前付きパラメーターです。名前付きパラメーターを使用した後のコード

//joinToString内部声明
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
    return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}
fun main(args: Array<String>) {
    val num = listOf(1, 2, 3)
    println(num.joinToString(separator = ",", prefix = "<", postfix = ">", transform = { "index$it" }))
}

4.ラムダ式の戻り値

ラムダ式の戻り値は、常に関数本体内の最後の行の式の値を返します

package com.mikyou.kotlin.lambda

fun main(args: Array<String>) {

    val isOddNumber = { number: Int ->
        println("number is $number")
        number % 2 == 1
    }

    println(isOddNumber.invoke(100))
}

関数本体の2つの式の位置を交換した後

package com.mikyou.kotlin.lambda

fun main(args: Array<String>) {

    val isOddNumber = { number: Int ->
        number % 2 == 1
        println("number is $number")
    }

    println(isOddNumber.invoke(100))
}

上記の例から、ラムダ式が関数本体の最後の行の式の値を返すことがわかります。println関数は値を返さないため、デフォルトでユニットタイプが出力されます。その内部原理は何ですか。実際、式の最後の行の戻り値タイプは、invoke関数の戻り値タイプとして使用されます。Javaコードへの逆コンパイルの上記の2つの方法を比較できます。

//互换位置之前的反编译代码
package com.mikyou.kotlin.lambda;

import kotlin.jvm.internal.Lambda;

@kotlin.Metadata(mv = {1, 1, 10}, bv = {1, 0, 2}, k = 3, d1 = {"\000\016\n\000\n\002\020\013\n\000\n\002\020\b\n\000\020\000\032\0020\0012\006\020\002\032\0020\003H\n¢\006\002\b\004"}, d2 = {"<anonymous>", "", "number", "", "invoke"})
final class LambdaReturnValueKt$main$isOddNumber$1 extends Lambda implements kotlin.jvm.functions.Function1<Integer, Boolean> {
    public final boolean invoke(int number) {//此时invoke函数返回值的类型是boolean,对应了Kotlin中的Boolean
        String str = "number is " + number;
        System.out.println(str);
        return number % 2 == 1;
    }

    public static final 1INSTANCE =new 1();

    LambdaReturnValueKt$main$isOddNumber$1() {
        super(1);
    }
}
//互换位置之后的反编译代码
package com.mikyou.kotlin.lambda;

import kotlin.jvm.internal.Lambda;

@kotlin.Metadata(mv = {1, 1, 10}, bv = {1, 0, 2}, k = 3, d1 = {"\000\016\n\000\n\002\020\002\n\000\n\002\020\b\n\000\020\000\032\0020\0012\006\020\002\032\0020\003H\n¢\006\002\b\004"}, d2 = {"<anonymous>", "", "number", "", "invoke"})
final class LambdaReturnValueKt$main$isOddNumber$1 extends Lambda implements kotlin.jvm.functions.Function1<Integer, kotlin.Unit> {
    public final void invoke(int number) {//此时invoke函数返回值的类型是void,对应了Kotlin中的Unit
        if (number % 2 != 1) {
        }
        String str = "number is " + number;
        System.out.println(str);
    }

    public static final 1INSTANCE =new 1();

    LambdaReturnValueKt$main$isOddNumber$1() {
        super(1);
    }
}

5.ラムダ式のタイプ

Kotlinは、関数タイプを定義するための簡潔な構文を提供します。

() -> Unit//表示无参数无返回值的Lambda表达式类型

(T) -> Unit//表示接收一个T类型参数,无返回值的Lambda表达式类型

(T) -> R//表示接收一个T类型参数,返回一个R类型值的Lambda表达式类型

(T, P) -> R//表示接收一个T类型和P类型的参数,返回一个R类型值的Lambda表达式类型

(T, (P,Q) -> S) -> R//表示接收一个T类型参数和一个接收P、Q类型两个参数并返回一个S类型的值的Lambda表达式类型参数,返回一个R类型值的Lambda表达式类型

上記のタイプの最初のものは理解しやすいはずです。最後のものになるのは少し難しいと推定されます。最後のものは実際には高階関数のカテゴリに属します。ただし、このタイプを個人的に確認する1つの方法は、タマネギをレイヤーごとに剥がして内側のレイヤーに分割する、つまり外側から内側を見て分割するのと少し似ています。ラムダ式タイプ自体の場合、一時的に全体として、最も外側のLambdaタイプを判別し、同様の方法を使用して内部で分割できます。

 

6. typealiasキーワードを使用して、Lambdaタイプに名前を付けます

複数のラムダ式を使用できるシナリオを想像してみましょう。ただし、これらのラムダ式の型はほとんど同じです。同じ大規模な一連のラムダ型をすべて繰り返すのは簡単です。そうでない場合、ラムダ型の宣言が長すぎて読みやすい。実際には必要ありません。すべての冗長な文法に対抗する言語であるKotlinの場合、一連のソリューションを提供し、コードの可読性を低下させることなくコードを簡素化できます。

fun main(args: Array<String>) {
    val oddNum:  (Int) -> Unit = {
        if (it % 2 == 1) {
            println(it)
        } else {
            println("is not a odd num")
        }
    }

    val evenNum:  (Int) -> Unit = {
        if (it % 2 == 0) {
            println(it)
        } else {
            println("is not a even num")
        }
    }

    oddNum.invoke(100)
    evenNum.invoke(100)
}

typealiasキーワード宣言を使用する(Int)->ユニットタイプ

package com.mikyou.kotlin.lambda

typealias NumPrint = (Int) -> Unit//注意:声明的位置在函数外部,package内部

fun main(args: Array<String>) {
    val oddNum: NumPrint = {
        if (it % 2 == 1) {
            println(it)
        } else {
            println("is not a odd num")
        }
    }

    val evenNum: NumPrint = {
        if (it % 2 == 0) {
            println(it)
        } else {
            println("is not a even num")
        }
    }

    oddNum.invoke(100)
    evenNum.invoke(100)
}

3つ目は、Kotlinのラムダ式でよく使用されるシーンです。

  • シナリオ1:ラムダ式はコレクションと一緒に使用されます。これは最も一般的なシナリオです。コレクションデータに対してさまざまなフィルタリング、マッピング、変換演算子、さまざまな操作を実行できます。非常に柔軟性があります。RxJavaを使用した開発者はすでに経験を積んでいると思います。この種の喜びは、Kotlinが言語レベルでライブラリを追加することなく関数型プログラミングをサポートするAPIを提供することは事実です。
package com.mikyou.kotlin.lambda

fun main(args: Array<String>) {
    val nameList = listOf("Kotlin", "Java", "Python", "JavaScript", "Scala", "C", "C++", "Go", "Swift")
    nameList.filter {
        it.startsWith("K")
    }.map {
        "$it is a very good language"
    }.forEach {
        println(it)
    }

}

 

 

 

  • シナリオ2:元の匿名内部クラスを置き換えますが、注意すべきことの1つは、クラスを置き換えることができるのは単一の抽象メソッドのみであるということです。
findViewById(R.id.submit).setOnClickListener(new View.OnClickListener() {
	@Override
	public void onClick(View v) {
		...
	}
});

kotlinラムダで実装

findViewById(R.id.submit).setOnClickListener{
    ...
}
  • シナリオ3:Kotlin拡張関数を定義する場合、または特定の操作または関数を値として渡す必要がある場合。
fun Context.showDialog(content: String = "", negativeText: String = "取消", positiveText: String = "确定", isCancelable: Boolean = false, negativeAction: (() -> Unit)? = null, positiveAction: (() -> Unit)? = null) {
	AlertDialog.build(this)
			.setMessage(content)
			.setNegativeButton(negativeText) { _, _ ->
				negativeAction?.invoke()
			}
			.setPositiveButton(positiveText) { _, _ ->
				positiveAction?.invoke()
			}
			.setCancelable(isCancelable)
			.create()
			.show()
}

fun Context.toggleSpFalse(key: String, func: () -> Unit) {
	if (!getSpBoolean(key)) {
		saveSpBoolean(key, true)
		func()
	}
}

fun <T : Any> Observable<T>.subscribeKt(success: ((successData: T) -> Unit)? = null, failure: ((failureError: RespException?) -> Unit)? = null): Subscription? {
	return transformThread()
			.subscribe(object : SBRespHandler<T>() {
				override fun onSuccess(data: T) {
					success?.invoke(data)
				}

				override fun onFailure(e: RespException?) {
					failure?.invoke(e)
				}
			})
}

4つ目は、アクセス変数と変数キャプチャのスコープにおけるKotlinのラムダ式です。

1.ローカル変数にアクセスするためのKotlinとJavaの内部クラスまたはラムダの違い

  • Javaの関数内で無名内部クラスまたはラムダを定義します。内部クラスがアクセスする関数ローカル変数はファイナライズする必要があります。つまり、関数ローカル変数の値を内部クラスまたはラムダ式で変更することはできません。非常に単純なAndroidイベントのクリック例を見ることができます
public class DemoActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        final int count = 0;//需要使用final修饰
        findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println(count);//在匿名OnClickListener类内部访问count必须要是final修饰
            }
        });
    }
}
  • Kotlinの関数内でラムダまたは内部クラスを定義すると、最終および非最終の変更された変数の両方にアクセスできます。つまり、関数のローカル変数の値をLambda内で直接変更できます。上記のKotlin実装の例

最終的に変更された変数にアクセスする

class Demo2Activity : AppCompatActivity() {

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_demo2)
		val count = 0//声明final
		btn_click.setOnClickListener {
			println(count)//访问final修饰的变量这个是和Java是保持一致的。
		}
	}
}

最終的に変更されていない変数にアクセスし、その値を変更します

class Demo2Activity : AppCompatActivity() {

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_demo2)
		var count = 0//声明非final类型
		btn_click.setOnClickListener {
			println(count++)//直接访问和修改非final类型的变量
		}
	}
}

上記の比較から、Kotlinでのラムダの使用はJavaでのラムダの使用よりも柔軟性があり、アクセスの制限が少ないことがわかります。これは、このブログの最初の文に答えるためです。Kotlinでのラムダ式は本当のサポートの意味です。パッケージですが、Javaのラムダはそうではありません。Kotlinのラムダ式はどのようにこれを行いますか?続けてください

2.Kotlinでのラムダ式の変数キャプチャと原理

  • 変数キャプチャとは何ですか?

上記の例から、Kotlinでは最終変数と非最終変数の両方にアクセスできることがわかります。原則は何ですか?その前に、まずラムダブ式の変数キャプチャと呼ばれる背の高い概念を捨てます実際、ラムダ式は関数の本体にある外部変数にアクセスできます。ラムダ式によってキャプチャされたこれらの外部変数を呼び出します。この概念により、上記の結論をより高くすることができます。

まず、Javaのラムダ式は最終的に変更された変数のみをキャプチャできます

次に、Kotlinのラムダ式は、最終的に変更された変数をキャプチャできるだけでなく、非最終的な変数にアクセスして変更することもできます

  • 可変キャプチャの原理

関数のローカル変数のライフサイクルがこの関数に属することは誰もが知っています。関数が実行されると、ローカル変数は破棄されますが、ローカル変数がラムダによってキャプチャされると、このローカル変数を使用するコードは次のようになります。後で再度実行します。つまり、キャプチャされたローカル変数はライフサイクルを遅らせる可能性があります。ラムダ式の最終的に変更されたローカル変数をキャプチャする原則は、ローカル変数の値と、この値を使用するラムダコードが保存されることです。一緒に;非最終変更ローカル変数をキャプチャする原則は、非最終ローカル変数が特別なラッパークラスによってラップされるため、この非最終変数をラッパークラスインスタンス、次にラッパークラスインスタンス参照を介して変更できることです。最終です。ラムダコードで保存します

上記の2番目の結論は、Kotlinの文法レベルでは正しいですが、実際の原理の観点からは間違っています。Kotlinがこれを文法レベルでシールドしているだけです。ラムダ式の実際の原理は、キャプチャすることしかできません。 Finalは変数を変更しますが、Kotlinが非Final変数の値を変更できるのはなぜですか?実際、Kotlinは文法レベルでブリッジングパッケージを作成しました。これは、いわゆる非Final変数をRefパッケージングクラスでラップし、保持します。それらは外部的にRefラッパーへの参照が最終的であり、ラムダはこの最終ラッパーの参照とともに保存され、ラムダ内の変数の値は最終ラッパー参照によって変更されます。

最後に、Kotlinが非最終ローカル変数を変更した逆コンパイルされたJavaコードを表示すると、一目でわかります。

class Demo2Activity : AppCompatActivity() {

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_demo2)
		var count = 0//声明非final类型
		btn_click.setOnClickListener {
			println(count++)//直接访问和修改非final类型的变量
		}
	}
}
@Metadata(
   mv = {1, 1, 9},
   bv = {1, 0, 2},
   k = 1,
   d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0012\u0010\u0003\u001a\u00020\u00042\b\u0010\u0005\u001a\u0004\u0018\u00010\u0006H\u0014¨\u0006\u0007"},
   d2 = {"Lcom/shanbay/prettyui/prettyui/Demo2Activity;", "Landroid/support/v7/app/AppCompatActivity;", "()V", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "production sources for module app"}
)
public final class Demo2Activity extends AppCompatActivity {
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361820);
      final IntRef count = new IntRef();//IntRef特殊的包装器类的类型,final修饰的IntRef的count引用
      count.element = 0;//包装器内部的非final变量element
      ((Button)this._$_findCachedViewById(id.btn_click)).setOnClickListener((OnClickListener)(new OnClickListener() {
         public final void onClick(View it) {
            int var2 = count.element++;//直接是通过IntRef的引用直接修改内部的非final变量的值,来达到语法层面的lambda直接修改非final局部变量的值
            System.out.println(var2);
         }
      }));
   }

   public View _$_findCachedViewById(int var1) {
      if(this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
      if(var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(Integer.valueOf(var1), var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if(this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }
}

3.Kotlinでラムダ式変数をキャプチャするための注意事項

注:Lambda式内のローカル変数の値を変更すると、Lambda式が実行されたときにのみトリガーされます。

5、Kotlinのラムダ式メンバーリファレンス

1.メンバー参照を使用する理由

Lambda式では、コードブロックをパラメーターとして関数に直接渡すことができることはわかっていますが、過去に名前付き関数として既に存在するコードブロックを渡したいというシナリオに遭遇したことがあります。今回は、コードブロックを繰り返し記述して渡す必要がありますか?絶対にそうではありませんが、Kotlinは繰り返しのコードを拒否します。したがって、メンバー参照の置換のみが必要です。

fun main(args: Array<String>) {
    val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
    println(persons.maxBy({ p: Person -> p.age }))
}

と置き換えることができます

fun main(args: Array<String>) {
    val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
    println(persons.maxBy(Person::age))//成员引用的类型和maxBy传入的lambda表达式类型一致
}

2.メンバー参照の基本構文

メンバー参照は、クラス、二重コロン、およびメンバーの3つの部分で構成されます。

3.メンバー参照の使用シナリオ

  • メンバー参照を使用する最も一般的な方法は、クラス名+ダブルコロン+メンバー(属性または関数)です。
fun main(args: Array<String>) {
    val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
    println(persons.maxBy(Person::age))//成员引用的类型和maxBy传入的lambda表达式类型一致
}
  • クラス名を省略し、最上位の関数を直接参照します(前のブログには特別な分析があります)
package com.mikyou.kotlin.lambda

fun salute() = print("salute")

fun main(args: Array<String>) {
    run(::salute)
}
  • メンバー参照は、関数を拡張するために使用されます
fun Person.isChild() = age < 18

fun main(args: Array<String>){
    val isChild = Person::isChild
    println(isChild)
}

この時点で、Kotlinラムダに関する基本的な知識は基本的に終了します。次の記事では、Lambdaとバイトコードの本質を分析し、Lambda式のパフォーマンスを最適化します。

おすすめ

転載: blog.csdn.net/az44yao/article/details/112921282