Kotlin基本知识(六)——lambda编程

270 阅读7分钟

一、Lambda简介

(1)函数代码块

函数式编码提供了一种解决问题的方法:把函数当作值来对待。

  • 用匿名内部类是实现监听
button.setOnClickListener(new OnClickListener) {
    @Override
    public void onClick(View view) {
        /* 点击后执行的动作 */
    }
}
  • 用lambda实现监听器
button.setOnClickListener { /* 点击后执行的动作 */ }

(2)集合

  • 手动在集合中搜索
// 定义
fun findTheOldest(people: List<Person>) {
    // 存储最大年龄
    var maxAge = 0
    // 存储年龄最大的人
    var theOldest: Person? = null
    
    for(person in people) {
        // 如果下一个人比现在年龄最大的人还要大,改变最大值
        if(person.age > maxAge) {
            maxAge = person.age
            theOldest = person
        }
    }
    
    println(theOldest)
}

// 测试
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> findTheOldest(people)
Person(name=Bob, age=31)
  • 用lambda在集合中搜索
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
// 比较年龄找到最大的元素
>>> println(people.maxBy { it.age })
Person(name="Bob", age=31)

maxBy函数可以在任何集合上调用,且只需要一个实参:一个函数,指定比较哪个值找到最大元素。

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

  • 用成员引用搜索
people.maxBy(Person::age)

(3)Lambda表达式的语法

|---- 参数 ----|  |-函数体-|
{ x: Int, y: Int -> x + y}
| --- 始终在花括号内 --- |

    lambda表达式的语法

Kotlin的lambda表达式始终用花括号包围。注意实参并没有用括号起来。箭头把实参列表和lambda的函数体隔开。

lambda表达式可以存储在一个变量中,把额这个变量当作普通函数对待(即通过相应实参调用它)

>>> val sum = { x: Int, y: Int -> x + y }
// 调用保存在变量中的lambda
>>> println(sum(1, 2))
3
  • 没有任何简明语法来重写这个例子
people.maxBy({ p: Person -> p.age })

Kotlin的语法约定:

    1. 如果lambda表达式是函数调用的最后一个实参,它可以放在括号的外边。
    1. 当lambda是函数唯一的实参时,还可以去掉调用代码中的空括号对。
    1. 若lambda是唯一的实参时,可以生路这些括号。
    1. 若lambda有多个实参时,既可以把lambda留着括号内来强调它是一个是实参,也可以把它放在括号的外面。
// 语法约定一
people.maxBy({ p: Person -> p.age})

// 语法约定二
people.maxBy() {p: Person -> p.age}
  • 把lambda放在括号外传递
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> val names = People.joinToString(separator = " ",
                            transform = {p: Person -> p.name})
>>> println(names)
Alice Bob
  • 把lambda放在括号外传递
people.joinToString(" ") { p: Person -> p.name }
// 显式地写出参数类型
people.maxBy { p: Person -> p.age }
// 推导出参数类型
people.maxBy { p -> p.age }

技巧:遵循这样一条简单的规则:先不声明类型,等编译器报错后再指定它们

  • 使用默认参数名称
// “it”是自动生成的参数名称
people.maxBy { it.age }

仅在实参名称没有显式地指定时这个默认的名称才会生成。

it约定能大大缩短你的代码,但你不应该滥用它。尤其是在嵌套lambda的情况下,最好显式地声明每个lambda的参数,否则很难搞清楚it引用的到底是哪个值

(4)在作用域中访问变量

当在函数内声明一个匿名内部类时,能够在这个匿名类内部引这个函数的参数和局部变量。也可以用lambda做同样的事。如果在函数内部使用lambda,也可以访问这个函数的参数,还有在lambda之前定义的局部变量

  • 在lambda中使用函数参数
// 定义
fun printMessageWithPrefix(messages: Collection<String>, prefix: String) {
    messages.forEach {
        println("$prefix $it")
    }
}

// 测试
>>> val errors = listOf("403 Forbidden", "404 Not Found")
>>> printMessagesWithPrefix(errors, "Error:")
Error: 403 Forbidden
Error: 404 Not Found
  • 区别: Kotlin和Java的一个显著区别就是,在Kotlin中不会限于访问final变量,在lambda内部也可以修改这些变量。

  • 在lambda中改变局部变量

// 定义
fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    
    responses.forEach {
        if(it.startsWith("4")) clientErrors ++
        else if(it.startsWith("5")) serverErrors ++
    }
    
    println("$clientErrors client errors, $serverErrors server errors")
}

// 测试
>>> val response = listOf("200 OK", "418 I'm a teapot", "500 Internal Server Error")
>>> printProblemCounts(responses)
1 client errors, 1 server errors

和Java不一样,Kotlin允许在lambda内部访问非final变量甚至修改它们

注意

默认情况下,局部变量的生命周期被限制在声明这个变量的函数中。但若它被lambda捕抓了,使用这个变量的代码可以被存储并稍后再执行。

捕抓final变量时,它的值和使用这个值的lambda代码一起存储。

对非final变量来说,它的值被封装在一个特殊的包装器中,这样你就可以改变这个值,而对这个包装器的引用会和lambda代码一起存储。

补充说明:为啥Java的匿名内部类只能访问final变量

内部类对象的生命周期会超过局部变量的生命周期。局部变量的生命周期:当该方法被调用时,该方法中的局部变量在栈中被创建,当方法调用结束时,退栈,这些局部变量全部死亡。而内部类对象生命周期与其它类一样:自创建一个匿名内部类对象,系统为该对象分配内存,直到没有引用变量指向分配给该对象的内存,它才会死亡(被JVM垃圾回收)。所以完全可能出现的一种情况是:成员方法已调用结束,局部变量已死亡,但匿名内部类的对象仍然活着。这不被JAVA允许。

(内容来源于:JAVA匿名内部类不能访问外部类方法中的局部变量,除非变量被声明为final类型)

Java 只允许你捕抓final变量。当你想捕抓可变变量时,可以使用下面两种技巧:要么声明一个单元素的数组,其中存储可变值;要么创建一个包装类的实例,其中存储要改变的值的引用。

这里有一个重要的注意事项:

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

  • 示例
fun tryToCountButtonClicks(button: Button): Int {
    var clicks = 0
    button.onClick { clicks ++}
    return clicksd
}

这个方法始终返回0,。尽管onClick处理器可以修改clicks的值,你并不能观察到值的变化,因为onClick处理器是在函数返回之后调用的。

正确的做法是把点击次数存储在函数外依然可以访问的地方——例如类的属性,而不是存储在函数的局部变量中。

(5) 成员引用

::运算符:把函数转换成一个值

val getAge = Person::age

这种表达式称为成员引用,它提供了简明语法,来创建一个调用单个方法或者访问单个属性的函数值。 双冒号把类名称与你要引用的成员(一个方法或者一个属性)名称隔开。

| 类 |   |成员|
Person :: age
  |用冒号隔开|

这是一个更简洁的lambda表达式,它做同样的事情:

val getAge = { person: Person :: age}

注意,不管你引用的是函数还是属性,都不要成员引用的名称后面加括号

  • 1.成员引用和调用该函数的lambda具有一样的类型,可以互换使用:people.maxBy(Person::age)
  • 2.引用顶层函数(不是类的成员):fun salate() = println("Salute!") run(::salute)。省略类名称的情况下,直接以::开头。成员引用::salute被当作实参传递给库函数run,它会调用相应的函数。
  • 3.lambda委托给一个接受多个参数的函数,提供成员引用代替他将会非常方便。
// 这个lambda委托给sendEmail函数
val action = { person: Person, message: String -> 
      sendEmail(person, message)
// 可以用成员引用替代
val nextAction = ::sendEmail
  • 4.用构造方法引用存储或者延期执行创建类实例的动作。构造方法引用的形式是在双冒号后制定类名称:
// 定义
data class Person(val name: String, val age: Int)

// 测试
>>> val createPerson = ::Person
>>> val p = createPerson("Alice", 29)
>>> println(p)
Person(name=Alice, age=29)
  • 5.可以用同样的方法引用扩展函数:
fun Person.isAdult() = age >= 21
val predicate = Person::isAdult

尽管isAdult不是Person类的成员,还是可以通过引用访问它,这和访问实例的成员没什么两样:person.isAdult()