《Kotlin实战》-第05章:Lambda编程

第五章 Lambda编程

lambda本质就是可以传递给其他函数的一小段代码。

5.1 Lambda表达式和成员引用
5.1.1 Lambda简介:作为函数参数的代码块

在代码中存储和传递一小段行为是常有的任务,为了简洁的达成目的,函数式编程提出可以把函数当做值来对待,使用lambda表达式后会更加简洁。
代码对比示例:

//java
button.setOnClickListener(new OnClickListener(){
    @Override
    public void onClick(View view){
        //点击后执行的动作
    }
})
//kotlin用lambda
button.setOnClickListener{/*点击后执行的动作/*}
5.1.2 Lambda和集合

对集合执行的大部分任务遵循几个通用模式,是个很好的展现lambda的场景。
下面的只是代码示例,看一哈即可。详细解读在后面章节。

//比较年龄找到最大的元素
//省略常规for循环,直接看lambda代码感受
data class Person(val name:String,val age:Int)
val people = listOf(Person("A",29),Person("B",33))
println(people.maxBy{it.age})
>>>>> People(name = A,age = B)

特别说明下maxBy,现在1.4版本已经升级为maxByOrNull。
下面为maxByOrNull的源码实现,上面的lambda表达式就是参数。

public inline fun <T, R : Comparable<R>> Iterable<T>.maxByOrNull(selector: (T) -> R): T? {
    val iterator = iterator()
    if (!iterator.hasNext()) return null
    var maxElem = iterator.next()
    if (!iterator.hasNext()) return maxElem
    var maxValue = selector(maxElem)
    do {
        val e = iterator.next()
        val v = selector(e)
        if (maxValue < v) {
            maxElem = e
            maxValue = v
        }
    } while (iterator.hasNext())
    return maxElem
}
5.1.3 Lambda表达式的语法

{x:Int,y:Int -> x+y}
lambda表达式语法要点

  • 始终用{}包围
  • 箭头前部分是实参,实参没有用括号括起来
  • 箭头后是函数体
  • 可以把lambda表达式存储在一个变量中,把这个变量当做普通函数对待
val sum = {x:Int,y:Int -> x+y}
println(sum(1,2))
  • 可以直接调用lambda表达式,有两种形式
    • { println(42) }() 不直观,可读性差
    • run{ println(42) }
  • 如果lambda表达式是函数调用的最后一个实参,就可以把它放到括号的外边
    • people.maxBy() {p:Person -> p.age}
  • 当lambda是函数唯一的实参时,还可以去掉代码中的空括号对
    • people.maxBy{p:Person -> p.age}
  • 如果lambda参数的类型可以被推导出来,就可以去掉参数类型显示。当不能被推导时,就不能去掉。一个规则是先不声明类型,等编译器报错再指定它们。
    • people.maxBy{p -> p.age}
  • 当实参名称没有显式地指定时,可以使用默认参数名称it代替
    • people.maxBy{it.age}
    • it约定能大大缩短代码,但不应该滥用,最好显式声明每个参数,否则在一些情况,如嵌套lambda时难以搞清it引用的值。
  • 同其他表达式一样,lambda表达式最后一行就是表达式的返回值
5.1.4 在作用域中访问变量

如果在函数内部使用lambda,lambda也可以访问这个函数的参数,还有在lambda之前定义的局部变量。
kotlin中不仅限于访问这些变量,还可以在lambda内部修改这些变量。Kotlin中称这些变量被lambda捕捉。
有几个注意的点:

  • 局部变量的生命周期被限制在函数中,当被lambda捕捉时,这个变量的代码可以被存储并稍后执行。
    • 原理:对于final变量,就是和lambda代码一起存储;非final变量,它的值被封装在一个特殊的包装器中。这个包装器的引用会和lambda代码一起存储。
  • 如果lambda被用在异步执行的情况,如响应事件处理器,对局部变量的修改只会在lambda执行的时候发生。
5.1.4 成员引用

1.概念解释
创建一个调用单个函数或者单个属性的函数值。
2.表现形式:用双冒号把类名称与要引用的成员名称隔开,如Person::age。
一定注意后面是没有括号的
3.代码示例

val getAge = Person::age
//等同于
val getAge = {person:Person -> person.age}
//使用
people.maxBy(Person::age)

4.几个特殊情况
4.1引用顶层函数,去掉不存在的类名称

fun salute() = println("Salute!")
run(::salute)

4.2如果lambda被委托给一个接受多个参数的函数,提供成员引用会非常方便

val action = {person:Person, message:String -> sendEmail(person,message)}
val action = ::sendEmail

4.3可以用构造方法引用存储或者延期执行创建类实例的动作

val createPerson = ::Person
val p = createPerson("a",22)
println(p)

4.4同样适用于扩展函数

fun Person.isAdult() = age >= 21
val predicate = Person::isAdult
5.2 集合的函数式API

函数式编程风格在操作集合时提供了很多优势。下面这些函数并不是Kotlin设计者发明的,而是存在于所有支持lambda的语言中。

5.2.1 基础:filter和map

1.filter:遍历集合并选出应用给定lambda后会返回true的那些元素
示例1:筛选偶数

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

示例2:筛选范围

val people = listOf(Person("a,"29),Person("b",31),Person("c",40))
println(peop;e.filter{it.age >30 })
>>>>
[Person(name=b,age=31),Person(name=c,age=40)]

2.map:对集合中的每一个元素应用给定的函数并把结果收集到一个新集合。
示例1:平方数

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

示例2:打印名字

val people = listOf(Person("a,"29),Person("b",31),Person("c",40))
println(people.map{it.name })
>>>>
[a,b,c]
//可以用成员引用重写为
people.map(Person::name)

2.1.特别的四个函数

  • filterKeys:根据key过滤map中的元素,it指key
  • filterValues:根据value过滤map中的元素,it指value
  • mapKeys:操作每一个元素里的key,it指每一对元素
  • mapValues:操作每一个元素里的value,it指每一对元素
    示例
val numbers = mapOf('a' to "aim",'b' to "bill",'c' to "coin")
     println(numbers.mapKeys{it.key + 1})
     println(numbers.mapValues{it})
     println(numbers.filterKeys{it>'b'})
     println(numbers.filterValues{it.length>3})
>>>>
{b=aim, c=bill, d=coin}
{a=a=aim, b=b=bill, c=c=coin}
{c=coin}
{b=bill, c=coin}
5.2.2 all、any、count和find:对集合应用判断式

1.概念说明

  • all:是否所有元素都满足判断式
  • any:集合中是否至少存在一个匹配的元素
  • count:检查有多少元素满足判断式
  • find:返回第一个符合条件的元素
    2.示例
println(people.all{it.age >30 })
println(people.any{it.age >30 })
println(people.count{it.age >30 })
println(people.find{it.age >30 })
>>>>>
false
true
2
Person(name=b,age=31)
5.2.3 groupBy:把列表转换成分组的map

1.概念
把所有元素按照不同特征划分成不同的分组。
2.示例

val people = listOf(Person("a,"29),Person("b",31),Person("c",29))
println(people.groupBy{it.age })
>>>>
[29=[Person(name=a,age=29),Person(name=c,age=29)],
31=[Person(name=b,age=31)]
5.2.4 flatMap和flatten:处理嵌套集合中的元素

1.概念说明

  • flatMap:首先根据作为实参给定的函数对集合中的每个元素做变换,然后把多个列表合并,重复元素依然出现。
  • flatten:相对于flatMap来说就是没有变换,直接合并。但只能对元素是list的list进行。

2.示例

val strings = listOf("abc","cde","f")
     println(strings.flatMap{it.toList()})
     val strings2 = listOf("11","22","33")
     val listOfLists = listOf(strings,strings2)
     println(listOfLists.flatten())
>>>>>
[a, b, c, c, d, e, f]
[abc, cde, f, 11, 22, 33]
5.3 惰性集合操作:序列

1.产生背景
对集合进行链式函数调用时,这些函数会及早地创建中间集合。这些中间集合会被存储在临时列表中。当集合中数据较大时,计算性能和内存开销会受到很大影响。
2.惰性集合操作
kotlin的惰性集合操作入口就是Sequence接口。这个接口表示的就是一个可以逐个列举元素的序列。Sequence只提供了一个方法:iterator。
3.转换
调用扩展函数asSequence把任意集合转换成序列,调用toList进行反向转换。

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

1.序列操作分为两种

  • 中间操作:返回的是一个序列,始终是惰性的。
  • 末端操作:返回的是一个结果,会触发执行所有的延期计算。

2.示例

listOf(1,2,3,4).asSequence()
    .map{
        println("map$it")
        it*it
    }
    .filter{
        println("filter$it")
        it%2 == 0
    }
    .toList()
//在不加最后这个toList时,不会打印任何东西
>>>>
map1
filter1
map2
filter4
map3
filter9
map4
filter16

3.调用顺序
不同的调用顺序对链式调用影响很大,不论对于序列和list都是如此。
要认真考虑是否可以优化调用顺序,以优化计算效率。
如进行filter和map时,如果map前filter后,则一定会执行map对所有元素进行操作后再进行筛选,而先filter后map的话则会先筛选元素后进行map转换。

5.3.2 创建序列

除了在集合上调用asSequence外,还可以用generateSequence函数来创建序列。
1.给定序列中的前一个元素,计算下一个元素。

val numbers = generateSequence(0){it + 1}
val numbersTo100 = numbers.takeWhile{ it<=100 }
println(numbersTo100.sum())
>>>>
5050

2.父序列:元素的父元素和它的类型相同。如下面例子查询文件是否在隐藏目录中

fun File.isInsideHiddenDirectory() = generateSequence(this){ it.parentFile }.any{ it.isHidden }
val file = File("/use/dddd/.....")
println(file.isInsideHiddenDirectory())
5.4 使用Java函数式接口

两个概念注意

  • 函数式接口:又称SAM接口,SAM为单抽象方法,即接口只有一个抽象方法。
  • 函数类型:Java没有函数类型,Kotlin有完全的函数类型,需要接受lambda作为参数的是就是用函数类型。
    • Kotlin不支持把lambda自动转换成实现Kotlin接口的对象。
    • 在后面8.1章节会讨论函数类型声明的用法
5.4.1 把lambda当做参数传递给Java方法

1.可以在Kotlin中把lambda传给Java中任何期望函数式接口的方法。
2.通过显式地创建一个实现了接口匿名对象也能达到效果
3.上述两种的不同在于

  • 使用lambda时,如果lambda没有访问任何来自定义它的函数的变量,相应的匿名类实例只会被创建一次,可以在多次调用之间重用。而如果lambda进行了外围作用域的变量捕捉,那么会在每次调用时创建一个新对象。
  • 使用显式声明对象时,每次调用都会创建一个新的实例

4.上面所说的为lambda创建一个匿名类,是只适用于期望SAM接口的Java方法,对集合使用Kotlin扩展方法的方式无效。
5.示例:

/* Java */
void postCompute(int delay,Runnable computation)
/* Kotlin */
postCompute(1000){ println(42) }
//同样方式可行,但显式声明匿名对象会导致每次都创建新对象
postCompute(1000,object:Runnable{
    override fun run(){
        println(42)
    }
})
//完全等价的方式
val runnable = Runable{ println(42) }
postCompute(1000,runnable)
5.4.2 SAM构造方法:显式地把lambda转换成函数式接口

1.概念说明
SAM构造方法:名称和函数式接口的名称一样,只接受一个参数,这个参数是被用作函数式接口单抽象方法的lambda。返回实现了这个接口的类的一个实例。

2.应用场景:编译器不会自动应用转换的时候

  • 如果一个方法返回的是一个函数式接口的实例,不能直接返回一个lambda时。
  • 需要把从lambda生成的函数式接口的类的实例存储在一个变量中时。
  • 尽管方法转换中SAM转换一般会自动发生,但总会有编译器不能正确重载转换的情况,这时就需要用显式地SAM构造方法来解决错误。

3.示例

//应用1
fun createDoneRunnable():Runnable{
    return Runnable{ println("done") }
}
createDoneRunnable().run()
//应用2
val listener = OnClickListener{ view -> when(view.id){ ..... } }
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)
5.5 带接受者的lambda:with和apply
5.5.1 with函数

1.结构说明
接受两个参数,第二个参数是个lambda,第一个参数会被转换作为第二个参数的接受者,可以显式地通过this引用访问这个接受者。当然,也可以省略this,直接访问这个值得方法和属性。
2.示例

//带this
fun alphabet():String{
    val stringBuilder = StringBuilder()
    return with(stringBuilder){
        for(letter in "A".."Z"){
            this.append(letter)
        }
        this.toString()
    }
}
//不带this,且用表达式函数体
fun alphabet() = with(stringBuilder){
        for(letter in "A".."Z"){
            append(letter)
        }
        toString()
    }

3.注意点

  • lambda是一种类似普通函数的定义行为的方式,而带接收者的lambda是类似扩展函数的定义行为的方式。
  • 当方法名称冲突时,可以用this加上显式标签,如想引用的是外部类的toString方法时this@OuterClass.toString()
5.5.2 apply函数

1.结构说明
apply函数被声明成一个扩展函数,在Kotlin中可以在任意对象上使用,不需要特别支持。
with函数返回的是执行lambda代码的结果,apply始终会返回接受者对象。
2.示例

//重构上面例子
fun alphabet() = StringBuilder().apply{
        for(letter in "A".."Z"){
            append(letter)
        }
    }.toString()
//初始化TextView,也可以把setPadding放进括号里
TextView(context).apply{
    text = "Sample Text"
    textSize = 20.0
}.setPadding(10,0,0,10)

5.6 小结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值