Programación Lambda en Kotlin parte 2

1. Expresiones lambda y referencias de miembros

1) Introducción a Lambda: bloques de código como parámetros de función

Las expresiones lambda se utilizan en escenarios como "ejecutar este controlador de eventos cuando ocurra un evento" o "aplicar esta operación a todos los elementos de esta estructura de datos". Las expresiones lambda pueden pasar eficientemente bloques de código directamente como parámetros de función. Al comparar la siguiente clase interna anónima con el oyente implementado por lambda, se puede ver que la implementación de lambda es más concisa,

/* Java 匿名内部类方式 */
button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        /* 点击后执行的动作 */
    }
});
复制代码
/* Kotlin Lambda方式 */
button.setOnClickListener{ /* 点击后执行的动作 */ }
复制代码

2) Lambdas y colecciones

Use lambda para buscar en la colección, por ejemplo, de la siguiente manera

val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy { it.age })
复制代码

La función maxBy se puede llamar en cualquier colección y requiere solo un argumento: una función. El código entre llaves { it.age } es la lambda que implementa esta lógica, toma un elemento de una colección como argumento (referencia con él) y devuelve el valor con el que comparar.

Si la lambda resulta ser el delegado de una función o propiedad, se puede reemplazar con una referencia de miembro, como

people.maxBy { Person::age }
复制代码

3) Sintaxis de la Expresión Lambda

Una lambda codifica una pequeña parte del comportamiento, se puede pasar como un valor, se puede declarar de forma independiente y almacenar en una variable, se puede declarar directamente y pasar a una función.

Una expresión lambda se define entre llaves, incluidos los parámetros y el cuerpo de una función, y una flecha separa la lista de parámetros real del cuerpo de la función lambda, de la siguiente manera:

image.pngPuede almacenar una expresión lambda en una variable y tratar esta variable como una función normal, como

val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2))
复制代码

Las expresiones lambda se pueden llamar directamente, como

{ println(42) }()
复制代码

Si necesita encerrar un pequeño fragmento de código en un bloque de código, puede usar la función de biblioteca ejecutar para ejecutar la lambda que se le pasó.La función de ejecutar es ejecutar el código en el cierre y devolver el valor, como

run { println(42) }
复制代码

La sintaxis de "people.maxBy { it.age }" se presenta paso a paso:

a) Si no se utiliza una sintaxis concisa, se escribe de la siguiente manera

people.maxBy({ p: Person -> p.age })
复制代码

Es un poco detallado, en primer lugar, demasiada puntuación arruina la legibilidad. En segundo lugar, el tipo se puede inferir del contexto y se puede omitir. Nuevamente, no es necesario asignar un nombre al parámetro de lambda. Finalmente, si la expresión lambda es el último argumento de una llamada de función, se puede colocar fuera de los paréntesis.

b) Si la expresión lambda es el último argumento de la llamada de función, se puede colocar fuera de los paréntesis, escrito de la siguiente manera

people.maxBy() { p: Person -> p.age }
复制代码

Cuando hay varios argumentos, puede dejar la lambda dentro o fuera de los paréntesis. Si está pasando dos o más lambdas, no puede poner más de una lambda afuera.

c)当lambda是函数唯一的实参时,还可以去掉调用代码中的空括号对:

people.maxBy { p: Person -> p.age }
复制代码

d)省略lambda参数类型:

people.maxBy { p -> p.age }
复制代码

e)在实参名称没有显示地指定,且只有一个参数,类型可推导时,使用默认参数名称it代替命名参数:

people.maxBy { it.age }
复制代码

在嵌套lambda的情况下,建议显式地声明每个lambda的参数。

如果用变量存储lambda,没有可以推断出参数类型的上下文,必须显式地指定参数类型,如,

val getAge = { p: Person -> p.age }
people.maxBy(getAge)
复制代码

lambda包含更多的语句时,最后一个表达式就是lambda的结果,如

val sum = { x: Int, y: Int ->
    println("Computing the sum of $x and $y...")
    x + y
}
println(sum(1, 2))
复制代码

4)在作用域中访问变量

在函数内部使用lambda,可以访问这个函数的参数,还有在lambda之前定义的局部变量。如下

fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
    messages.forEach {
        println("$prefix $it")
    }
}
复制代码

该函数接收lambda作为实参,指中可以访问""定对每个元素的操作,在lambda中可以访问"prefix"参数。

在Kotlin中lambda内部可以访问final变量,并且可以修改非final变量。如下,

fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    responses.forEach {
        if (it.startsWith(“4”)) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
}
复制代码

默认情况下,局部变量的生命周期被限制在声明这个变量的函数中,但是如果它被lambda捕捉了,使用这个变量的代码可以被存储并稍后再执行。当捕捉final变量时,它的值和使用这个值的lambda代码一起存储。而对非final变量来说,它的值被封装在一个特殊的包装器中,这样就可以改变这个值,对这个包装器的引用会和lambda代码一起存储。

如果lambda被用作事件处理器或者用在其他异步执行的情况,对局部变量的修改只会在lambda执行的时候发生。

5)成员引用

把函数转换成一个值,可以作为其他函数的参数传递,使用::来转换,如

val getAge = Person::age
复制代码

该表达式称为成员引用,用于创建一个调用单个方法或者访问单个属性的函数值。双冒号把类名称与要引用的成员名称隔开。

还可以引用顶层函数,如

fun salute() = println("Salute")
run(::salute)
复制代码

可以用构造方法引用存储或者延期执行创建类实例的动作,构造方法引用的形式是在双冒号后指定类名称,如下创建"Person"实例的动作被保存成了值,

data class Person(val name: String, val age: Int)
val createPerson = ::Person
val p = createPerson("Alice", 29)
复制代码

还可以用同样的方式引用扩展函数,

fun Person.isAdult() = age >= 21
val predicate = Person::isAdult
复制代码

如下两种方式等价,

val p = Person("Dmitry", 34)

//方式一
val personsAgeFunction = Person::age
personsAgeFunction(p)

//方式二
val dmitryAgeFunction = p::age
dmitryAgeFunction()
复制代码

二、集合的函数式API

1)基础:filter和map

filter函数遍历集合并选出应用给定lambda后会返回true的那些元素,如

val list = listOf(1, 2, 3, 4)
list.filter { it % 2 == 0 }
复制代码

map函数对集合中的每一个元素应用给定的函数并把结果收集到一个新集合,如

val list = listOf(1, 2, 3, 4)
list.map { it * it }
复制代码

2) "all" "any" "count"和"find":对集合应用判断式

all:是否所有元素都满足判断式,如下结果为false

val canBeInClub27 = { p: Person -> p.age <= 27}
val people = listOf(Person("Alice", 27), Person("Bob", 31))
people.all(canBeInClub27)
复制代码

any:检查集合中是否至少存在一个匹配的元素,如下结果为true

people.any(canBeInClub27)
复制代码

count:检查有多少元素满足判断式,如下结果为1

people.count(canBeInClub27)
复制代码

count与size的区别:size会创建一个中间集合用来存储所有满足判断式的元素,count只是跟踪匹配元素的数量,更高效。

find:返回第一个符合条件的元素,如下结果为Person(name=Alice, age=27)

people.find(canBeInClub27)
复制代码

3)groupBy:把列表转换成分组的map

如下例子

val people = listOf(Person("Alice", 31), Person("Bob", 29), Person("Carol", 31))
println(people.groupBy { it.age })
复制代码

输出为

{29=[Person(name=Bob, age=29)], 
 31=[Person(name=Alice, age=31), Person(name=Carol, age=31)]}
复制代码

每一个分组都是存储在一个列表中,结果类型就是Map<Int, List>。

4)flatMap和flatten:处理嵌套集合中的元素

flatMap首先根据作为实参给定的函数对集合中的每个元素做变换,然后把多个列表合并成一个列表。如下结果为[a, b, c, d, e, f],

val strings = listOf("abc", "def")
strings.flatMap { it.toList() }
复制代码

三、惰性集合操作:序列

filter和map会创建中间集合,每一步的中间结果都被存储在一个临时列表,数据量大时,性能很差。为了提高效率,把操作变成使用序列,如下

people.asSequence()
    .map(Person::name)
    .filter { it.startWith("A") }
    .toList()
复制代码

通过asSequence把初始集合转换成序列,通过toList把结果序列转换回列表。如果只需要迭代序列中的元素,可以直接使用序列。如果要使用其他的API方法,比如用下标访问元素,需要把序列转换成列表。

Kotlin惰性集合操作的入口就是asSequence接口,表示的就是一个可以逐个列举元素的元素序列。Sequence只提供了一个方法,iterator,用来从序列中获取值。

1)执行序列操作:中间和末端操作

序列操作分为两类:中间的和末端的。一次中间操作返回的是另一个序列,一次末端操作返回的是一个结果。没有末端操作时,不会在控制台输出任何内容,末端操作触发执行了所有的延期计算。所有操作按顺序应用在每一个元素上。

四、使用Java函数式接口

Kotlin的lambda可以无缝地和Java API互操作。

先看一个点击监听的例子,

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) { ... }
}
复制代码

在Kotlin中,可以传递一个lambda,代替这个实例,如下

button.setOnClickListener { view -> ...}
复制代码

这种方式可以工作的原因是 OnClickListener 接口只有一个抽象方法。这种接口被称为函数式接口,或者SAM接口,SAM代表单抽象方法。Kotlin允许你在调用接收函数式接口作为参数的方法时使用lambda,来保证Kotlin代码既整洁又符合习惯。

1)把lambda当作参数传递给Java方法

可以把lambda传给任何期望函数式接口的方法。例如,下面这个方法,它有一Runnable类型的参数:

/* Java */
void postponeComputat on(int delay, Runnable computation);
复制代码

显式地创建一个实现了Runnable的匿名对象进行传参,如下

postponeComputation(1000, object : Runnable {
    override fun run() {
        println(42)
    }
})
复制代码

在Kotlin中,可以调用它并把一个lambda作为实参传给它。编译器会自动把它转换成一个Runnable的实例:

postponeComputation(1000) { println(42) }
复制代码

方式一显示地声明对象时,每次调用都会创建一个新的实例。方式二使用lambda情况不同:如果lambda没有访问任何来自定义它的函数的变量,相应的匿名类实例可以在多次调用之间重用,整个程序只会创建一个Runnable的实例。

为了让方式一只创建一个实例,可以把Runnable实例存储在一个变量中,每次调用时都使用这个变量:

val runnable = Runnable { println(42) }
fun handleComputation() {
    postponeComputation(1000, runnable)
}
复制代码

runnable存储程序中唯一的Runnable实例,每次postponeComputation调用时用的是同一个对象。

如果lambda从包围它的作用域中捕捉了变量,每次调用时编译器都要创建一个新对象,其中存储着被捕捉的变量的值。示例如下,

fun handleComputation(id: String) {
    postponeComputation(1000) { println(id) }
}
复制代码

上述代码在底层会被编译成如下形式,

class HandleComputation$1(val id: String) : Runnable {
    override fun run() {
        println(id)
    }
}

fun handleComputation(id: String) {
    postponeComputation(1000, HandleComputation$1(id))
}
复制代码

2)SAM构造方法:显示地把lambda转换成函数式接口

SAM相关知识见链接函数式接口(SAM 接口) - Kotlin 语言中文站 (kotlincn.net)

lambda是一个代码块,不是一个对象,也不能把它当成对象引用。lambda中的this引用指向的是包围它的类。对比匿名对象内,this关键字指向该对象实例。

把lambda作为参数传给一个重载方法时,也有编译器不能选择正确的重载的情况,这时,使用显示的SAM构造方法是解决编译器错误的好方法。

五、“with” 、 “let” 、“run” 、“also” 、“apply” 、 “use”

下面函数都可以运行闭包中的代码并相应有返回结果。

1)“with”函数

“with”函数示例如下,

fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
    toString()
}
复制代码

with函数是一个接收两个参数的函数:这个例子中两个参数分别是创建的一个StringBuilder对象和一个lambda。这里把lambda放在括号外面。

with函数把它的第一个参数转换成作为第二个参数传给它的lambda的接收者,则StringBuilder作为lambda的接收者。可以显示地通过this引用来访问这个接收者,也可以省略this引用。

with返回的值是执行lambda代码的结果,该结果是lambda中的最后一个表达式的值。

2)“let”函数

let函数把一个调用它的对象变成lambda表达式的参数,如图所示,

image.png 安全调用“let”只在表达式不为null时执行lambda,如下示例

email?.let { sendEmailTo(it) }
复制代码

let函数只在email的值非空时才被调用,返回值是函数里面最后一行,或者指定return。当需要检查多个值是否为null时,可以用嵌套的let调用来处理。但这种代码可能较难以理解,用if可能更简单。

3)“run”函数

run函数结合了let和with函数的优点:可以在表达式执行前进行非null校验;传入参数为this,可以直接调用this的属性和函数;返回的值是执行lambda代码的结果,该结果是lambda中的最后一个表达式的值。

user?.run {
    println("$name, $age")
    this
}
复制代码

4)“also”函数

“also”函数可以在表达式执行前进行非null校验;传入参数为it;返回值固定为this自身类型。

user?.also {
    println("${it.name}, ${it.age}")
}
复制代码

5)“apply”函数

“apply”函数可以在表达式执行前进行非null校验;传入参数为this;返回值固定为this自身类型。

user?.apply {
    println("$name, $age")
}
复制代码

6)“use”函数

当InputStream、OutputStream等各种打开了需要关闭的东西,通过use函数执行lambda代码,执行完后,use会帮助关闭需要关闭的东西,如

File(pathName).inputStream().reader().buffered()
    .use {
        it.readLine()
    }
复制代码

关于it和this

  • this 用于带接收者的函数类型,表示接收者。
  • it 用于函数类型中:函数只有一个参数。it表示参数对象

Supongo que te gusta

Origin juejin.im/post/7086073433699647524
Recomendado
Clasificación