Kotlin篇二之Lambda编程

一、Lambda表达式和成员引用

1)Lambda简介:作为函数参数的代码块

lambda表达式使用场景如“当一个事件发生时运行这个事件处理器”或“把这个操作应用到这个数据结构中所有的元素上”。lambda表达式可以高效地直接传递代码块作为函数参数。对比如下匿名内部类和lambda实现的监听器,可以看出lambda实现更简洁,

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

2)Lambda和集合

用lambda在集合中搜索,举例如下

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

maxBy函数可以在任何集合上调用,且只需要一个实参:一个函数。花括号中的代码{ it.age }就是实现了这个逻辑的lambda,它接收一个集合中的元素作为实参(使用it引用它)并且返回用来比较的值。

如果lambda刚好是函数或者属性的委托,可以用成员引用替换,如

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

3)Lambda表达式的语法

一个lambda把一小段行为进行编码,能把它当作值到处传递,可以被独立地声明并存储到一个变量中,可以直接声明它并传递给函数。

lambda表达式定义在花括号中,包括参数和函数体,箭头把实参列表和lambda的函数体隔开,如下,

image.png 可以把lambda表达式存储在一个变量中,把这个变量当作普通函数对待,如

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

可以直接调用lambda表达式,如

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

如果需要把一小段代码封闭在一个代码块中,可以使用库函数run来执行传给它的lambda,run的作用为运行闭包中的代码并返回值,如

run { println(42) }
复制代码

下面按步骤推出“people.maxBy { it.age }”的语法:

a) 如果不使用任何简明语法,写法如下

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

该写法有点啰嗦,首先,过多的标点符号破坏了可读性。其次,类型可以从上下文推断出来并可以省略。再次,不需要给lambda的参数分配一个名称。最后,如果lambda表达式是函数调用的最后一个实参,可以放到括号的外边。

b)如果lambda表达式是函数调用的最后一个实参,可以放到括号的外边,写法如下

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

当有多个实参时,既可以把lambda留在括号内,也可以放在括号外。如果要传递两个或更多的lambda,不能把超过一个的lambda放在外面。

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表示参数对象

猜你喜欢

转载自juejin.im/post/7086073433699647524
今日推荐