Skip to content

Commit

Permalink
Update 2.Kotlin_高阶函数&Lambda&内联函数.md
Browse files Browse the repository at this point in the history
  • Loading branch information
CharonChui authored May 14, 2024
1 parent d903180 commit 7d12b58
Showing 1 changed file with 62 additions and 36 deletions.
98 changes: 62 additions & 36 deletions KotlinCourse/2.Kotlin_高阶函数&Lambda&内联函数.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ fun <T> lock(lock: Lock, body: () -> T): T {

调用lock函数时,可以传入另一个函数作为参数。

和许多编程语言类似,如果函数的最后一个参数也是函数,则该函数参数还可以定义在括号外面,如:
***和许多编程语言类似,如果函数的最后一个参数也是函数,则该函数参数还可以定义在括号外面***,如:
```kotlin
val result = lock(lock, { sharedResource.operation()})
// 等同于
Expand All @@ -42,7 +42,7 @@ lock(lock) {

高阶函数类似C语言的函数指针,它的另一个使用场景是map函数。

如果函数只有一个参数,则可以忽略声明的函数参数,用it来代替。
***如果函数只有一个参数,则可以忽略声明的函数参数,用it来代替。***

```kotlin
val doubled = mMap.map {it -> it * 2}
Expand Down Expand Up @@ -183,7 +183,8 @@ class CountryTest {

- 需要把isBigEuropeanCountry的方法引用当做参数传递给filterCountries

Kotlin存在一种特殊的语法,通过两个冒号来实现对于某个类的方法进行引用(方法引用表达式)。以上面的代码为例,假如我们有一个CountryTest类的对象实例countryTest,如果要引用它的isBigEuropeanCountry方法,就可以这样写:
**Kotlin存在一种特殊的语法,通过两个冒号来实现对于某个类的方法进行引用(方法引用表达式)。**
以上面的代码为例,假如我们有一个CountryTest类的对象实例countryTest,如果要引用它的isBigEuropeanCountry方法,就可以这样写:

```kotlin
countryTest::isBigEuropeanCountry
Expand Down Expand Up @@ -307,26 +308,27 @@ val foo = { x: Int ->
```java
// 没有使用Lambda的老方法:
button.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent ae){
System.out.println("Actiondetected");
}
public void actionPerformed(ActionEvent ae){
System.out.println("Actiondetected");
}
});
// 使用Lambda:
button.addActionListener(()->{
System.out.println("Actiondetected");
button.addActionListener(() -> {
System.out.println("Actiondetected");
});


// 不采用Lambda的老方法:
Runnable runnable1=new Runnable(){
@Override
public void run(){
System.out.println("RunningwithoutLambda");
}
Runnable runnable1 = new Runnable() {
@Override
public void run() {
System.out.println("RunningwithoutLambda");
}
};

// 使用Lambda:
Runnable runnable2=()->{
System.out.println("RunningfromLambda");
Runnable runnable2 = () -> {
System.out.println("RunningfromLambda");
};
```

Expand Down Expand Up @@ -357,10 +359,10 @@ max(strings, compare)
```
就是找出`strings`里面最长的那个。但是我个人觉得`compare`还是很碍眼的,因为我并不想在后面引用他,那我怎么办呢,就是用“匿名函数”方式。
```kotlin
max(strings, (a,b)->{a.length < b.length})
max(strings, (a,b) -> {a.length < b.length})
```

`(a,b)->{a.length < b.length}`就是一个没有名字的函数,直接作为参数赋给`max`方法的第二个参数。但这个方法有很多东西都没有写明,如:
`(a,b) -> {a.length < b.length}`就是一个没有名字的函数,直接作为参数赋给`max`方法的第二个参数。但这个方法有很多东西都没有写明,如:

- 参数的类型
- 返回值的类型
Expand Down Expand Up @@ -409,7 +411,7 @@ val msg = { x: Int -> "xxx" }
(Int) -> String
```

如果将lambda赋值给一个变量,编译器会根据该lambda来推测变量的类型,如上例所示。然而,就像任何其他类型的对象一样,你可以显式地定义该变量的类型。例如,以下代码定义了一个变量add,该变量可以保存对具有两个Int参数并返回Int类型的lambda的引用
如果将lambda赋值给一个变量,编译器会根据该lambda来推测变量的类型,如上例所示。然而,就像任何其他类型的对象一样,你可以显式地定义该变量的类型。例如,以下代码定义了一个变量add,该变量可以保存对具有两个Int参数并返回Int类型的lambda的引用:

```kotlin
val add: (Int, Int) -> Int
Expand All @@ -420,7 +422,8 @@ add = { x: Int, y: Int -> x + y }
Lambda类型也被认为是函数类型。


当Lambda表达式的参数列表中只有一个参数时,那么可以不声明唯一的参数名,而是可以使用it关键字来代替,因为Kotlin会隐含的声明一个名为it的参数.
**当Lambda表达式的参数列表中只有一个参数时,那么可以不声明唯一的参数名,而是可以使用it关键字来代替,因为Kotlin会隐含的声明一个名为it的参数.这叫做单个参数的隐式名称,代表了这个Lambda所接收的单个参数**

```kotlin
val list = listOf("Apple", "Bnana", "Orange", "Pear")
val maxLengthFruit = list.maxBy {it.length}
Expand All @@ -435,13 +438,13 @@ fun foo(int: Int) = {
listOf(1, 2, 3).forEach { foo(it) } // 对一个整数列表的元素遍历调用foo
```

这里,你可定会纳闷it是啥?其实它也是Kotlin简化Lambda表达的一种语法糖,叫做单个参数的隐式名称,代表了这个Lambda所接收的单个参数。这里的调用等价于:
这里的调用等价于:

```kotlin
listOf(1, 2, 3).forEach { item -> foo(item) }
```

如果lambda具有一个单独的参数,而且编译器能够推断其类型,你可以省略该参数,并在lambda的主体中使用关键字it指代它。要了解它是如何工作的,如前所述,假设使用以下代码将lambda赋值给变量:
假设使用以下代码将lambda赋值给变量:

```kotlin
val addFive: (Int) -> Int = { x -> x + 5 }
Expand All @@ -453,7 +456,11 @@ val addFive: (Int) -> Int = { x -> x + 5 }
val addFive: (Int) -> Int = { it + 5 }
```

在上述代码中,{it+5}等价于{x->x+5},但更加简洁。请注意,你只能在编译器能够推断该参数类型的情况下使用it语法。例如,以下代码将无法编译,因为编译器不知道it应该是什么类型:
在上述代码中,{it+5}等价于{x->x+5},但更加简洁。

请注意,你只能在编译器能够推断该参数类型的情况下使用it语法。

例如,以下代码将无法编译,因为编译器不知道it应该是什么类型:

```kotlin
val addFive = { it + 5 } // 该代码无法编译,因为编译器不能推断其类型
Expand All @@ -463,7 +470,7 @@ val addFive = { it + 5 } // 该代码无法编译,因为编译器不能推断



我们看一下foo函数用IDE转换后的Java代码:
我们看一下foo函数用IDE转换后的Java代码:

```java
@JvmStatic
Expand Down Expand Up @@ -520,13 +527,14 @@ listOf(1, 2, 3).forEach{ foo(it)() }
## 闭包


闭包就是能够读取其他函数内部变量的函数。
**闭包就是能够读取其他函数内部变量的函数。**

它是函数内部和函数外部信息交换的桥梁。

在Kotlin中,Lambda表达式或匿名函数(局部函数、对象表达式等)都可以访问它的必报
在Kotlin中,Lambda表达式或匿名函数(局部函数、对象表达式等)都可以访问它的闭包

在Kotlin中,你会发现匿名函数体、Lambda在语法上都存在“{}",由这对花括号包裹的代码如果访问了外部环境变量则被称为一个闭包。一个闭包可以被当做参数传递或直接使用,它可以简单的看成”访问外部环境变量的函数“。Lambda是Kotlin中最常见的闭包形式。
在Kotlin中,你会发现匿名函数体、Lambda在语法上都存在“{}",由这对花括号包裹的代码如果访问了外部环境变量则被称为一个闭包。
一个闭包可以被当做参数传递或直接使用,它可以简单的看成”访问外部环境变量的函数“。Lambda是Kotlin中最常见的闭包形式。

与Java不一样的地方在于,Kotlin中的闭包不仅可以访问外部变量,还能够对其进行修改(我有点疑惑,Java为啥不能修改?下面说),如下:

Expand All @@ -539,8 +547,6 @@ listOf(1, 2, 3).filter { it > 0 }.forEach {
println(sum) // 6
```

看到这里我是懵逼的? 到底什么是闭包? 闭包有什么作用?

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。--百度百科
第一句总结的很简洁了:闭包就是能够读取其他函数内部变量的函数。

Expand Down Expand Up @@ -623,7 +629,7 @@ What java docs says about “Local Inner class cannot access the non-final local



JVM create create a synthetic field inside the inner class in java -
JVM create a synthetic field inside the inner class in java -

As final variable will not change after initialization, when a inner class access final local variables **compiler create a synthetic field inside the inner class and also copy that variable into the heap.** So, these synthetic fields can be accessed inside local inner class even when execution of method is over in java.

Expand Down Expand Up @@ -680,21 +686,31 @@ fun main() {

是不是发现了新世界的大门,内部函数很轻松地调用了外部变量a。

这只是一个最简单的闭包实现。按照这种思想,其他的实现例如:函数、条件语句、Lambda表达式等等都可以理解为闭包,这里不再赘述。不过万变不离其宗,只要记得一句话:**闭包就是能够读取其他函数内部变量的函数**。就是一个函数A可以访问另一个函数B的局部变量,即便另一个函数B执行完成了也没关系。目前把满足这样条件的函数A叫做闭包。
这只是一个最简单的闭包实现。按照这种思想,其他的实现例如:函数、条件语句、Lambda表达式等等都可以理解为闭包,这里不再赘述。

不过万变不离其宗,只要记得一句话:**闭包就是能够读取其他函数内部变量的函数**。就是一个函数A可以访问另一个函数B的局部变量,即便另一个函数B执行完成了也没关系。目前把满足这样条件的函数A叫做闭包。





## 内联函数

刚被闭包搞蒙,这里又没搞明白内联函数到底是干什么? 有什么作用?Kotlin中的内联函数其实显得有点尴尬,因为它之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。然而,在Java中我们似乎并不需要特别关注这个问题,因为在Java 7之后,JVM引入了一种叫做invokedynamic的技术,它会自动帮助我们做Lambda优化。但是为什么Kotlin要引入内联函数这种手动的语法呢? 这主要还是因为Kotlin要兼容Java 6。
刚被闭包搞蒙,这里又没搞明白内联函数到底是干什么? 有什么作用?Kotlin中的内联函数其实显得有点尴尬,因为它之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。

然而,在Java中我们似乎并不需要特别关注这个问题,因为在Java 7之后,JVM引入了一种叫做invokedynamic的技术,它会自动帮助我们做Lambda优化。但是为什么Kotlin要引入内联函数这种手动的语法呢? 这主要还是因为Kotlin要兼容Java 6。



## 优化Lambda开销

在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类(也就是说我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式)。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的匿名类对象。可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。
在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类(也就是说我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式)。

该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的匿名类对象。

可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。

尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。

#### 1. invokedynamic

Expand All @@ -710,7 +726,9 @@ fun main() {

invokedynamic固然不错,但Kotlin不支持它的理由似乎也很充分,我们有足够的理由相信,其最大的原因是Kotlin在一开始就需要兼容Android最主流的Java版本SE 6,这导致它无法通过invovkedynamic来解决Android平台的Lambda开销问题。

因此,作为另一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。
因此,作为另一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。

简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。
所以内联函数的工作原理并不复杂,就是Kotlin编译器会将内敛函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。

所以如果你想在用Kotlin开发时获得尽可能良好的性能支持,以及控制匿名类的生成数量,就有必要来学习下内联函数的相关语法。
Expand Down Expand Up @@ -975,7 +993,9 @@ fun main() {
println("main end")
}
```
这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。现在我们就刚好传入一个空的字符串参数,运行程序,打印结果如下:
这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。

注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。现在我们就刚好传入一个空的字符串参数,运行程序,打印结果如下:
```
main start
printString begin
Expand Down Expand Up @@ -1024,7 +1044,9 @@ inline fun runRunnable(block: () -> Unit) {

![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/inline_noline_error.png?raw=true)

这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。
这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。

而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。


而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。
Expand Down Expand Up @@ -1064,7 +1086,11 @@ Error: (2, 11) Kotlin: 'return' is not allowed here

#### 具体化参数类型

除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型。Kotlin与Java一样,由于运行时的类型擦除,我们并不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型。我们可以用reified修饰符来实现这一效果。
除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型。

**Kotlin与Java一样,由于运行时的类型擦除,我们并不能直接获取一个参数的类型。**

**然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型。我们可以用reified修饰符来实现这一效果。**

```kotlin
fun main(args: Array<String>) {
Expand Down

0 comments on commit 7d12b58

Please sign in to comment.