Detaillierte Erläuterung der Kotlin-Funktionen höherer Ordnung

Funktion höherer Ordnung

In Kotlin sind Funktionen erstklassige Bürger, und Funktionen höherer Ordnung stellen in Kotlin eine große Schwierigkeit dar. Wenn Sie Funktionen höherer Ordnung nicht verstehen, wird es sehr schwierig sein, Coroutinen in Kotlin zu lernen und den Quellcode von Kotlin zu lesen , weil es zu viele Funktionen höherer Ordnung gibt.

Definition von Funktionen höherer Ordnung

Die Definition einer Funktion höherer Ordnung ist sehr einfach: Eine Funktion ist eine Funktion höherer Ordnung, wenn ihr Parametertyp eine Funktion oder ihr Rückgabewerttyp eine Funktion ist .

Funktionstyp

In Kotlin gibt es den Integer-Typ Int und den String-Typ String. Ebenso haben Funktionen auch Typen. Zum Beispiel:

fun add(num1: Int, num2: Int): Int {
    return num1 + num2
}

Der Funktionstyp dieser Additionsfunktion ist  (Int, Int) -> Int. Der Funktionstyp abstrahiert tatsächlich den „Parametertyp“ und den „Rückgabewerttyp“ der Funktion . Da (Int, Int) -> Int ein Funktionstyp ist, kann er mit Ganzzahl verwendet werden. Zeichen Definieren Sie eine Variable wie den Zeichenfolgentyp als Funktionstyp. Wie unten gezeigt, ist der Typ der Variablen c der Funktionstyp. Zu diesem Zeitpunkt meldet der Compiler keinen Fehler, sodass der Variablentyp auf festgelegt werden kann der Funktionstyp.

Wie weist man der Variablen c einen Wert zu? Analog zur Zuweisung von Integer- und String-Variablen müssen wir, um einer Funktionstypvariablen einen Wert zuzuweisen, der Variablen eine Funktionsreferenz mit demselben Funktionstyp zuweisen. Die spezifische Schreibmethode ist wie folgt:

val c: (Int, Int) -> Int = ::add

fun add(num1: Int, num2: Int): Int = num1 + num2

::add Diese Schreibweise ist eine Schreibweise mit Funktionsreferenzen.

Zusätzlich zu Funktionsreferenzen unterstützt Kotlin auch die Verwendung von Lambda-Ausdrücken, um Variablen eines Funktionstyps Werte zuzuweisen . Wie folgt:

val c: (Int, Int) -> Int = {num1: Int, num2: Int -> num1 + num2}

In tatsächlichen Projekten verwenden wir in den meisten Fällen Lambda-Ausdrücke, um Funktionen höherer Ordnung aufzurufen.

Syntaxstruktur des Lambda-Ausdrucks: {Parametername 1: Parametertyp, Parametername 2: Parametertyp -> Funktionskörper} Jede Codezeile kann in den Funktionskörper geschrieben werden, und die letzte Codezeile wird automatisch als Rückgabewert verwendet des Lambda-Ausdrucks

Nachdem wir die Definition von Funktionstypen und Funktionen höherer Ordnung verstanden haben , können wir leicht Funktionen höherer Ordnung definieren, wie unten gezeigt:

// 参数是函数类型的高阶函数
fun higherFunction(func: (Int, Int) -> Int) {
    
}
// 返回值是函数类型的高阶函数
fun higherFunction(): (Int, Int) -> Int {

}

Funktionsaufrufe höherer Ordnung

Nehmen wir als Beispiel die Array-Durchquerung in Kotlin, um über Funktionsaufrufe höherer Ordnung zu sprechen.

Zuerst definieren wir ein Array vom Typ Int wie folgt:

val intArray = intArrayOf(1, 2, 3, 4, 5)

Wir verwenden zum Durchlaufen nicht die for in-Methode, sondern zum Durchlaufen die forEach-Methode. Die forEach-Funktion ist eine Funktion höherer Ordnung. Der Quellcode lautet wie folgt:

public inline fun IntArray.forEach(action: (Int) -> Unit): Unit {
    for (element in this) action(element)

Zunächst einmal muss eine Funktion höherer Ordnung eine Funktion sein, daher ist es definitiv kein Problem, die Methode wie folgt aufzurufen:

intArray.forEach(?)

Es ist nur so, dass dies   ein Parameter vom Funktionstyp ist. Der Funktionstyp ist (Int) -> Einheit. Dann würde ich einfach eine Variable desselben Funktionstyps definieren und sie an forEach übergeben, wie unten gezeigt:

val action: (Int) -> Unit = ??

fun main() {
    intArray.forEach(action)
}

Durch das obige Lernen wissen wir, dass es sich  ?? um eine Funktionsreferenz oder einen Lambda-Ausdruck handeln kann. Wenn wir eine Funktionsreferenz verwenden, sieht der Code folgendermaßen aus:

val action: (Int) -> Unit = ::printValue

fun main() {
    intArray.forEach(action)
}

fun printValue(value: Int): Unit {
    println(value)
}

Wie bereits erwähnt, verwenden wir in tatsächlichen Projekten in den meisten Fällen Lambda-Ausdrücke, um Funktionen höherer Ordnung aufzurufen. Da Funktionsreferenzen problematisch sind, müssen wir zum Aufrufen von Funktionen höherer Ordnung eine spezielle Funktion schreiben. Und es gibt viele einfache Möglichkeiten, Lambda-Ausdrücke zu schreiben.

Wir verwenden Lambda-Ausdrücke, um den obigen Code wie folgt umzuschreiben:

val action: (Int) -> Unit = {value: Int -> println(value)}

fun main() {
    intArray.forEach(action)
}

Es gibt viele einfache Möglichkeiten, Lambda-Ausdrücke zu schreiben. Jetzt vereinfachen wir {value: Int -> println(value)}:

  1. Kotlin verfügt über einen Typ-Push-Mechanismus, sodass Int entfernt werden kann
val action: (Int) -> Unit = {value -> println(value)}
  1. Wenn der Lambda-Ausdruck nur einen Parameter hat, kann er direkt durch diesen ersetzt werden und es ist nicht erforderlich, den Parameternamen zu deklarieren.
val action: (Int) -> Unit = {println(it)}

Durch Ersetzen des vereinfachten Codes sieht der obige Code nun wie folgt aus:

fun main() {
    intArray.forEach({println(it)})
}

Dieser Code kann auch vereinfacht werden:

  1. Wenn der Lambda-Parameter der letzte Parameter der Funktion ist, kann der Lambda-Ausdruck außerhalb der Funktionsklammern verschoben werden
fun main() {
    intArray.forEach(){
        println(it)
    }
}
  1. Wenn der Lambda-Ausdruck der einzige Parameter der Funktion ist, können Sie die Klammern der Funktion auch weglassen
fun main() {
    intArray.forEach{
        println(it)
    }
}

An dieser Stelle kann es nicht mehr vereinfacht werden. Dies ist die endgültige Version. Im Vergleich zu dem, wie es am Anfang aussah, ist dieser Code bereits sehr prägnant.

Funktionstyp mit Empfänger

Wir haben die höherwertige Funktion forEach bereits erwähnt. Schauen wir uns die höherwertige Funktion apply an, um zu sehen, was der Unterschied zwischen den beiden ist. Der Quellcode der apply-Funktion lautet wie folgt:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

Der von der Apply-Funktion empfangene Funktionstyp ist  T.() -> Uniteins mehr als der Funktionstyp, den wir zuvor gesehen haben  T.T. Was bewirkt das also?

Bevor wir über ihre Funktion sprechen, schauen wir uns auch eine Funktion höherer Ordnung an. Diese Funktionen höherer Ordnung sind in der Kotlin-Standardbibliothek definiert. Ihr Zweck besteht darin, Codeblöcke innerhalb des Objektkontexts auszuführen. Der Quellcode der Also-Funktion lautet wie folgt folgt:

public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

Der von der Also-Funktion empfangene Funktionstyp ist  (T) -> Unit.

Schauen wir uns die praktischen Unterschiede zwischen diesen beiden Funktionen wie folgt an:

Angenommen, wir fügen hier User in das generische T, User.() -> Unit ein, was darauf hinweist, dass dieser Funktionstyp in der User-Klasse definiert ist. Welche Vorteile hat es also, den Funktionstyp hier in der User-Klasse zu definieren? Der Vorteil besteht darin, dass der Lambda-Ausdruck, der beim Aufruf der Apply-Funktion übergeben wird, automatisch den Kontext „User“ hat, sodass er ohne zusätzliche Qualifizierer auf die Mitglieder des Empfängerobjekts zugreifen kann.

Das ist zwar etwas abstrakt, aber ich denke, dass es anhand des obigen Bildes relativ leicht zu verstehen ist.

Bisher haben wir uns mit dem theoretischen Wissen über Funktionen höherer Ordnung befasst.

Anwendungen von Funktionen höherer Ordnung

Fall 1: Zählen Sie die Anzahl der Zeichen (ohne Leerzeichen) in der Datei

fun main() {
    File("build.gradle").readText() // 读文件,直接以 String 的格式返回
        .toCharArray()  // 将字符串转换成字符数组
        .filter { !it.isWhitespace() }  // 过滤空白字符
        .groupBy { it } // 按照集合中每个字符分组
        .map {it.key to it.value.size } // 映射,重新生成新的集合
        .let {
            println(it)
        }
}

Die Laufergebnisse sind wie folgt:

In diesem Fall haben wir die höherwertigen Funktionen Filter, GroupBy, Map und Let verwendet. Wenn Sie diese Schreibmethode nicht so gut verstehen, können Sie die Ergebnisse jedes Schritts ausdrucken und einen Blick darauf werfen.

Inline-Optimierung

Bevor wir darüber sprechen, was Inline-Optimierung ist, werfen wir zunächst einen Blick auf das Implementierungsprinzip von Funktionen höherer Ordnung. Wir wissen, dass Kotlin und Java vollständig kompatibel sind und schließlich in .class-Dateien kompiliert werden. Allerdings gibt es in Java kein Konzept für Funktionen höherer Ordnung. Wie würden also die Funktionen höherer Ordnung von Kotlin aussehen, wenn sie in Java-Code dekompiliert würden? ?

Beispiel: Schauen wir uns die folgende Funktion höherer Ordnung foo() an:

fun main() {
    var i = 0
    foo {
        i++
        println(i)
    }
}

fun foo(block: () -> Unit) {
    block()
}

Java-Code nach der Dekompilierung:

// 主要代码,省略了一些没用的代码
public final class HigherFunctionKt {
   public static final void main() {
      foo((Function0)(new Function0() {
         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }

         public final void invoke() {
            int var10001 = i.element++;
            int var1 = i.element;
            System.out.println(var1);
         }
      }));
   }

   public static final void foo(@NotNull Function0 block) {
      Intrinsics.checkNotNullParameter(block, "block");
      block.invoke();
   }
}

Funktion0 ist hier eine Schnittstelle. Sie können sehen, dass der Funktionstypparameter der Funktion foo höherer Ordnung zu Funktion0 geworden ist und der Funktionsaufruf höherer Ordnung in der Funktion main () auch zur Aufrufmethode der „anonymen inneren Klasse“ geworden ist. Daher werden Funktionen höherer Ordnung immer noch in Form anonymer innerer Klassen ausgeführt. Sind Kotlins Funktionen höherer Ordnung nur dazu gedacht, das Schreiben „anonymer innerer Klassen“ zu vereinfachen?

Natürlich nicht. Die Leistung der Funktionen höherer Ordnung von Kotlin ist viel höher als die anonymer innerer Klassen. In einigen extremen Fällen gibt es sogar eine Leistungsverbesserung um das Hundertfache. Natürlich kann unsere obige Implementierung die Leistung nicht verbessern, aber die Schreibmethode ist auch sehr einfach: Sie müssen lediglich ein Inline-Schlüsselwort vor der Funktion hinzufügen.

Lassen Sie uns testen, ob das Inline-Schlüsselwort die Leistung von Funktionen höherer Ordnung wirklich verbessern kann. Hier verwenden wir JMH zum Testen. Der Code lautet wie folgt:

@BenchmarkMode(Mode.Throughput) // 基准测试的模式,采用整体吞吐量的模式
@Warmup(iterations = 3) // 预热次数
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS) // 测试参数,iterations = 10 表示进行10轮测试
@Threads(8) // 每个进程中的测试线程数
@Fork(2)  // 进行 fork 的次数,表示 JMH 会 fork 出两个进程来进行测试
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 基准测试结果的时间类型
open class SequenceBenchmark {

    // 不用inline的高阶函数
    fun foo(block: () -> Unit) {
        block()
    }

    // 使用inline的高阶函数
    inline fun fooInline(block: () -> Unit) {
        block()
    }

    // 测试无inline的代码
    @Benchmark
    fun testNonInlined() {
        var i = 0
        foo {
            i++
        }

    }

    // 测试inline的代码
    @Benchmark
    fun testInlined() {
        var i = 0
        fooInline {
            i++
        }
    }
}

fun main() {

    val options = OptionsBuilder()
        .include(SequenceBenchmark::class.java.simpleName)
        .output("benchmark_sequence.log")
        .build()
    Runner(options).run()
}

Die Testergebnisse lauten wie folgt: Je höher die Punktzahl, desto besser die Leistung:

Aus den obigen Testergebnissen können wir ersehen, dass der Effizienzunterschied zwischen ihnen fast das 30-fache beträgt, unabhängig davon, ob Inline verwendet wird oder nicht. Und dies ist nur der einfachste Fall. Wenn mehrere Funktionen höherer Ordnung verschachtelt in einigen komplexen Codeszenarien ausgeführt werden, unterscheidet sich die Ausführungseffizienz zwischen ihnen um ein Hundertfaches.

Wenn wir die Funktionen auf zehn Ebenen verschachteln und erneut testen, werden wir feststellen, dass die Leistungslücke noch größer ist. Der Code lautet wie folgt:

@BenchmarkMode(Mode.Throughput) // 基准测试的模式,采用整体吞吐量的模式
@Warmup(iterations = 3) // 预热次数
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS) // 测试参数,iterations = 10 表示进行10轮测试
@Threads(8) // 每个进程中的测试线程数
@Fork(2)  // 进行 fork 的次数,表示 JMH 会 fork 出两个进程来进行测试
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 基准测试结果的时间类型
open class SequenceBenchmark {

    // 不用inline的高阶函数
    fun foo(block: () -> Unit) {
        block()
    }

    // 使用inline的高阶函数
    inline fun fooInline(block: () -> Unit) {
        block()
    }

   @Benchmarkfun testNonInlined() {var i = 0 foo { foo { foo { foo { foo { foo { foo { foo { foo { foo { i++ } } } } } } } } } }}
   
   @Benchmarkfun testInlined() { var i = 0 fooInline { fooInline { fooInline { fooInline { fooInline { fooInline { fooInline { fooInline { fooInline { fooInline {
}

fun main() {

    val options = OptionsBuilder()
        .include(SequenceBenchmark::class.java.simpleName)
        .output("benchmark_sequence.log")
        .build()
    Runner(options).run()
}

Die Testergebnisse sind wie folgt:

Aus den obigen Leistungstestdaten können wir ersehen, dass sich die Leistung unseres testInlined nach 10 Verschachtelungsebenen nahezu nicht verändert hat; und wenn testNonInlined 10 Verschachtelungsebenen aufweist, ist die Leistung sechsmal schlechter als bei einer Verschachtelungsebene. . Und derzeit beträgt der Leistungsunterschied zwischen den beiden Funktionen fast das 200-fache.

Wie verbessert das Inline-Schlüsselwort die Leistung von Funktionen höherer Ordnung so sehr?

Inline-Prinzip

Tatsächlich ist das Funktionsprinzip von Inline-Funktionen sehr einfach:  Der Kotlin-Compiler ersetzt den Code in der Inline-Funktion während der Kompilierung automatisch an der Stelle, an der er aufgerufen wird, sodass kein Laufzeitaufwand entsteht .

Nehmen Sie den folgenden Code als Beispiel:

// 使用inline的高阶函数
inline fun fooInline(block: () -> Unit) {
    block()
}

@Benchmark
fun testInlined() {
    var i = 0
    fooInline {
        fooInline {
            fooInline {
                fooInline {
                    fooInline {
                        fooInline {
                            fooInline {
                                fooInline {
                                    fooInline {
                                        fooInline {
                                            i++
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Nach dem Prinzip der Inline-Funktion entspricht der obige Code dem Folgenden:

// 使用inline的高阶函数
inline fun fooInline(block: () -> Unit) {
    block()
}

@Benchmark
fun testInlined() {
    var i = 0
    fooInline { 
        i++
    }
}

Nach 10 Verschachtelungsebenen hat sich die Leistung von testInlined also nahezu nicht geändert. Dasselbe gilt für die Dekompilierung dieses Codes in Java-Code:

@Benchmark
public final void testInlined() {
   int i = 0;
   int $i$f$fooInline = false;
   int var4 = false;
   int $i$f$fooInline = false;
   int var7 = false;
   int $i$f$fooInline = false;
   int var10 = false;
   int $i$f$fooInline = false;
   int var13 = false;
   int $i$f$fooInline = false;
   int var16 = false;
   int $i$f$fooInline = false;
   int var19 = false;
   int $i$f$fooInline = false;
   int var22 = false;
   int $i$f$fooInline = false;
   int var25 = false;
   int $i$f$fooInline = false;
   int var28 = false;
   int $i$f$fooInline = false;
   int var31 = false;
   int i = i + 1;
}

Zusammenfassen

Wenn die Parameter einer Funktion vom Funktionstyp sind oder der Rückgabewert vom Funktionstyp ist, handelt es sich bei der Funktion um eine Funktion höherer Ordnung. Funktionen höherer Ordnung können unseren Code vereinfachen, und die Verwendung des Schlüsselworts inline kann die Leistung von Funktionen höherer Ordnung verbessern.

Mehrere Funktionen höherer Ordnung, die wir häufig verwenden, sind in der Datei Standard.kt des Kotlin-Quellcodes definiert. Sie können einen Blick darauf werfen.

Supongo que te gusta

Origin blog.csdn.net/s_nshine/article/details/132463051
Recomendado
Clasificación