Lambda编程

Lambda表达式

Lambda的语法

lambda表达式的语法:{ 参数列表 -> 函数体 }。例如,{ x: Int, y: Int -> x + y }

可以将lambda表达式存储在一个变量中,把这个变量当作普通函数对待,就是可以通过相应实参来调用它。

val sum = { x: Int, y: Int -> x + y }
sum(1, 2)
1
2

lambda表达式大括号中的最后一句为它的返回值

可以使用Kotlin标准库函数run来直接执行lambda表达式。

run({ println("I am daking") })
1

作为函数参数的lambda

在你的代码中存储和传递一小段行为是常有的任务,如事件监听器、把某个操作应用到一个数据结构的所有元素上等。

在老版本的Java中(Java8之前),用匿名内部类来实现这个需求。

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        /* 点击后执行的动作 */
    }
});
1
2
3
4
5
6

函数式编程把函数当作值来对待,直接传递函数,而不需要像Java那样先声明一个类再传递这个类的实例。

button.setOnClickListener(fun (view) {
    /* 点击后执行的动作 */
})
1
2
3

使用Lambda表达式,不需要声明函数,可以高效地直接传递代码块作为函数参数。

button.setOnClickListener( { view -> /* 点击后执行的动作 */ } )
1

Lambda和集合

在Kotlin中,可在任何集合上调用maxBy函数,它只需要一个实参 —— 一个函数,指定比较哪个值来找到最大元素。可使用lambda来作为函数参数传入。

val people = listOf(Person("daking", 27), Person("wing", 26))
people.maxBy({ p: Person -> p.age })
1
2

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

people.maxBy(Person::age)
1

Lambda的语法简化

Lambda的语法可以根据以下各种情况来进行简化。

people.maxBy({ p: Person -> p.age })
1

如果lambda表达式是函数调用的最后一个实参,它可以放在括号的外边。

people.maxBy() { p: Person -> p.age }
1

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

people.maxBy { p: Person -> p.age }
1

如果lambda参数的类型可以被推导出来,就不需要显式地指定它。

people.maxBy { p -> p.age }
1

如果当前上下文期望的只有一个参数的lambda,且这个参数的类型可以推断出来,就可以用默认参数名称it来代替命名参数。

people.maxBy { it.age }
1

在作用域中访问变量

当在函数中声明一个匿名内部类时,能够在这个匿名类内部引用这个函数的参数和局部变量。

lambda也同样。如果在函数内部使用lambda,lambda也可以访问这个函数的参数和在lambda之前定义的局部变量。

fun printMsgWithPrefix(msgs: Collection<String>, prefix: String) {
    msgs.forEach {
        println("$prefix $it")
    }
}
1
2
3
4
5

Java只允许捕捉final变量,即Kotlin的val变量。但在Kotlin中,lambda不仅可以访问val变量,还可以访问var变量并修改它们。

fun printProblemCounts(response: Collection<Int>) {
    var errors = 0
    response.forEach {
        if (it != 0) {
            errors++
        }
    }
    println("errors: $errors")
}
1
2
3
4
5
6
7
8
9

默认情况下,局部变量的生命周期被限制在声明这个变量的函数中,但如果它被lambda捕捉,它的生命周期就与lambda相关。

实际上,lambda捕捉变量的实现细节为:

  • lambda捕捉了一个val变量,它的值被拷贝下来。

  • 当捕捉了一个var变量,它的值被作为Ref类的一个实例存储下来。Ref变量是val的能轻易地捕捉,然而实际值被存储在Ref字段中,可以在lambda中进行修改。

var counter = 0
val inc = { counter++ }
// 上面的两行代码等价于下面的三行代码
class Ref<T>(var value: T)
val counter = Ref(0)
val inc = { counter++ }
1
2
3
4
5
6

成员引用

使用成员引用表达式来创建一个调用单个方法或者访问单个属性的函数值。

将类中的某个方法或属性变成成员引用,语法为类名::成员名

Person::age // Person类中的age属性
Person::say // Person类中的say方法
1
2

可以引用顶层函数,语法为::顶层函数名

fun sayHi() = println("I am daking.")
run(::sayHi)
1
2

可以引用扩展函数,语法为接收者::扩展函数名

fun Person.isAdult() = age >= 18
val predicate = Person::isAdult
1
2

可以引用构造方法,语法为::类名称

data class Person(val name: String, val age: Int)

val createPerson = ::Person
val p = createPerson("daking", 27)
1
2
3
4

集合的函数式API

Kotlin标准库中定义了和集合有关的一些函数,来简化我们的业务代码。

集合的函数式API支持链式调用。

people.filter { it.age < 30 }
    .map { it.name }
1
2

过滤:filter

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

val list = listOf(1, 2, 3, 4)
println(list.filter { it % 2 == 0 }) // [2, 4]
1
2

filterKeys和filterValues可分别用于过滤map的键和值。

变换:map

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

val list = listOf(1, 2, 3, 4)
println(list.map { it * it }) // [1, 4, 9, 16]
1
2

mapKeys和mapValues可分别用于变换map的键和值。

判断:all、any、count、find

all函数是检查集合中的所有元素是否都满足判断式,返回Boolean。

any函数是检查集合中是否至少存在一个匹配的元素,返回Boolean。

count函数是计算集合中满足判断式的元素个数,返回Int。

find函数是找出集合中首个满足判断式的元素,返回元素。

分组:groupBy

groupBy函数把所有元素按照不同的特征划分成不同的分组。

// 根据年龄进行分组
val people = listOf(Person("daking", 27), Person("kun", 26), Person("wing", 26))
println(people.groupBy { it.age })
1
2
3

groupBy函数的操作结果是一个map,键是分组依据,值是元素分组。

{
    27=[Person{name=daking, age=27}],
    26=[Person{name=kun, age=26}, Person{name=wing, age=26}]
}
1
2
3
4

平铺:flatMap、flatten

flatMap函数主要做了两件事情:

  • 根据传入的函数对集合中的每个元素做变换操作。

  • 把多个列表合并成一个列表,即平铺。

val strings = listOf("abc", "def")
println(strings.flatMap { it.toList() }) // [a, b, c, d, e, f]
1
2

flatten不会对元素进行变换,直接对集合进行平铺操作。例如,将一个元素为列表的列表平铺成一个列表。listOfLists.flatten()

序列

序列的惰性求值

集合的函数式API支持链式调用,但每一步的中间结果都会被存储在一个临时集合中。如果原集合的元素个数比较多,那么链式调用就会变得十分低消。

序列中的元素求值是惰性的。因此,可以使用序列更高效地对集合元素执行链式操作,而不需要创建额外的集合来保存过程中产生的中间结果。

可以调用扩展函数asSequence把任意集合转换成序列,而序列可调用toList()来转换回列表。

people.asSequence()
    .filter { it.age < 30 }
    .map { it.name }
    .toList()
1
2
3
4

序列的操作

序列的操作分为:中间操作末端操作

中间操作返回的是另一个序列,这个新序列知道如何变换原始序列中的元素。

末端操作返回的是一个结果,这个结果可能是集合、元素、数字或其他从初始集合的变换序列中获取的任意对象。

序列的操作-w358

中间操作始终是惰性的。例如,执行下面的一段代码,并不会在控制台上输出任何内容,这意味着map和filter被延期了。

listOf(1, 2, 3, 4).asSequence()
    .map { print("map($it) "); it * it }
    .filter { print("filter($it) "); it % 2 == 0 }
1
2
3

末端操作触发执行所有的延期计算。例如,在上面一段代码的最后加上.toList()才会触发map和filter。

集合的函数式API是按照操作顺序,第一个操作处理完所有元素后再进行下一个操作,而序列不一样。序列是按照元素顺序,处理完第一个元素的所有操作,再进行下一个元素。例如,比较上面一段代码在集合和序列上的执行顺序。

  • 集合的执行顺序为:map(1) map(2) map(3) map(4) filter(1) filter(4) filter(9) filter(16)

  • 序列的执行顺序为:map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)

序列这种按照元素顺序来执行的好处可以通过map加find操作体现。

// 集合
listOf(1, 2, 3, 4).map { it * it }.find { it > 3 }
// 序列
listOf(1, 2, 3, 4).asSequence().map { it * it }.find { it > 3 }
1
2
3
4

序列的惰性求值的优势-w373

创建序列

在集合上调用asSequence()创建序列。

也可以使用generateSequence函数来创建序列。它接收两个参数,第一个为序列中的前一个元素,第二个为计算出下一个元素的函数。

使用generateSequence计算100以内所有自然数之和。

// (中间操作)序列的第一个元素为0,之后的元素为前一个元素+1
val naturalNumbers = generateSequence(0) { it + 1 }
// (中间操作)少于等于100的元素
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
// (末端操作)计算这个序列所有元素的和
println(numbersTo100.sum()) // 5050
1
2
3
4
5
6

如果元素的父元素和它的类型相同(比如人类或文件),可以使用generateSequence来创建一个由它所有祖先组成的序列。

fun File.getRootDir() =
    generateSequence(this) { it.parentFile } // 一个不断向上遍历父目录的序列
    .find { it.parentFile == null } // 当找到首个父目录为空的就停止,即是根目录
1
2
3

Java函数式接口

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

函数式接口是指只有一个抽象方法的接口。它也被称为SAM接口,SAM是Single Abstract Method的缩写。

public interface OnClickListener {
    void onClick(View v);
}
1
2
3

lambda作为参数传递给Java方法

Kotlin允许你在调用函数式接口作为参数的方法时使用lambda作为实参。

button.setOnClickListener(new OnClickListener() {
    @Override
    void onClick(View v) {
        // ...
    }
});
1
2
3
4
5
6
button.setOnClickListener { view -> ... }
1

上面第一种方案,通过显式地创建一个实现了OnClickListener接口的匿名对象,每次调用都会创建一个新的实例。而第二种方案,如果lambda没有访问任何来自定义它的函数的变量,相应的匿名类实例可以在多次调用之间重用。但如果lambda从包围它的作用域中捕捉了变量,每次调用就不再重用同一个实例了。

显式地把lambda转换成SAM接口

大多数情况下,从lambda到SAM接口实例的转换都是自动发生的,但也存在需要显式地执行转换的情况。例如,如果有一个方法返回的是SAM接口实例,不能直接返回一个lambda,要用SAM构造方法把它包装起来。

fun createToastListener(): OnClickListener {
    return OnClickListener { println("click") }
}
1
2
3

SAM构造方法只接收一个参数 —— 被用作函数式接口单抽象方法体的lambda,而且SAM构造方法会返回实现了这个接口的类的一个实例。

lambda和this

注意,lambda中的this引用指向的是包围它的类,没有办法引用到lambda转换成的匿名类实例。从编译器的角度来看,lambda是一个代码块,不是一个对象。

如果你的事件监听器在处理事件时还需要取消自己,不能使用lambda,要使用实现了接口的匿名对象等。因为在匿名对象内,this关键字指向该对象实例,可以把它传给移除监听器的API。

带接收者的lambda

最常用的带接收者的lambda就是with函数和apply函数。

with函数

很多语言都有这样的语句,可以用它对同一个对象执行多次操作,而不需要反复把对象的名称写出来。Kotlin利用标准库函数with来实现,而不是某种特殊的语言结构。

val result = StringBuilder()
result.append("I")
result.append("am")
result.append("daking")
1
2
3
4
val result = StringBuilder()
with(result) {
    append("I") // 相当于result.append()
    append("am")
    append("daking")
}
1
2
3
4
5
6

with结构看起来像是一种特殊的语法结构,但它实际上是一个接收两个参数的函数:一个对象和一个lambda。利用lambda可放在括号外的约定,让整个调用看起来就像是内建的语言功能。

with函数会把它的第一个参数(对象)转换成第二个参数(lambda)的接收者。在lambda中可以显式地通过this引用来访问这个接收者。另外,可以省略this引用,不用任何限定符直接访问这个接收者的方法和属性。

如果接收者的方法名和定义with的外部类的方法名相同,可通过this.方法名()this@外部类名.方法名()来区分。

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

apply函数

apply函数几乎和with函数一模一样,唯一的区别是apply始终会返回作为实参传递给它的对象,即apply始终返回接收者对象。

apply被声明成一个扩展函数,它的接收者会变成作为实参的lambda的接收者。可在任意对象上使用apply函数。

val result = StringBuilder().apply {
    append("I")
    append("am")
    append("daking")
}.toString()
1
2
3
4
5

apply函数让你可使用Builder风格的API创建和初始化任何对象。

fun createCommonTextView(context: Context) = 
    TextView(context).apply {
        text = "Text"
        textSize = 20.0
        setPadding(10, 0, 0, 0)
    }
1
2
3
4
5
6