函数
函数签名
函数签名是函数声明的一部分,它定义了函数的名称、参数和返回值。函数签名和函数体一起定义了完整的函数。
fun fib(n: Int): Long {
return if (n < 2) {
1
} else {
fib(n - 1) + fib(n - 2)
}
}
示例中,fun fib(n: Int): Long就是函数签名。kotlin中用fun关键字作为函数声明的开始,之后依次是函数名、参数(用括号括起的部分)、冒号和返回值。
扩展函数
在各种编程语言的各种辅助函数,目的是通过新添加的函数来扩展现有类的接口(API),以便在程序上下文中或一般情况下使用。在很多编程语言中,由于无法修改那些不属于你的泪,因此只能使用工具类这样的处理方式,去拓展他以实现自己想要的操作。
kotlin的扩展函数可以为现有的类(如Int或Date)直接添加方法或属性。
//创建并调用扩展函数
fun Date.plusDays(n: Int) = Date(this.time + n * 86400000)//添加n天到date
val now = Date()
val tomorrow = now.plusaDays(1)//扩展函数可以直接从Date的实例化对象now中被调用
想要创建扩展函数,可以像通常那样,使用fun关键字,但要在函数名前加上扩展函数所要扩展的类(在示例中是Date),在此处被称为接收者类,然后就可以像调用接收者类上已有方法那样,直接调用该方法。
作用域和导入
扩展函数和扩展属性通常创建在顶级位置,因此他们在整个文件中都可见。而实际上,他们可以像其他函数和变量一样被赋予可见性。
//导入扩展函数
import com.example.time.plusDays
示例中,假设上一示例的plusDays扩展函数定义在time包下的文件中,请注意,函数名是直接跟在包名之后的,因为扩展并不直接在类或对象中。如果包中定义了多个名为plusDays的扩展函数,那么这些所有的扩展函数都会被导入。
中缀函数(infix)
通常,kotlin的函数调用使用的是将参数放入括号的前缀表示法,但是对于两个参数的函数来说,可能希望将函数名放在参数之间,就像7+2这样,将运算符放在参数之间。
//中缀函数to
infix fun <A,B> A.to(that: B) = Pair(this, that)
示例中, A、B是长泛型函数,函数中的infix修饰符允许在调用函数时将函数名放在两个函数之间。
//调用中缀函数
val pair = "Kotlin" to "Android"
val userToScore = mapOf("Peter" to 0.82, "John" to 0.97)
示例中,第一行代码创建了一个Pair("Kotlin", "Android"),第二行代码展示了中缀函数的常见用法:通过辅助函数mapOf来实例化一个map对象,该map对象的初始化参数为可变参数类型的Pair对象, 该Pair对象是通过to函数创建的。在许多场景中,使用to函数来实例化Pair对象比使用构造函数更具有可读性。
也可以使用infix修饰符自定义中缀函数,但前提是该函数是类成员函数或扩展函数,并且只有一个额外的参数(就像前面的to函数一样)。例如,可以创建一个中缀函数,它使用标准库函数repeat来讲一个字符串复制指定的次数,如下例所示。
//自定义中缀函数
infix fun Int.times(str: String) = str.repear(this)
val message = 3 times "Kotlin " //运行结果:Kotlin Kotlin Kotlin
Lambda入门
如果匿名内部类只包含一个方法,那么语法会显得冗余。
Lambda表达式设计出来就是为了解决这一问题,同样也是匿名函数。
简介
普通函数,拥有4种返回值类型:无参无返回,无参有返回,有参无返回,有参有返回。
Lambda表达式,只有两种:无参有返回,有参有返回。
无参有返回
在定义时,只需将函数体写在“{}”中,函数体可以是表达式或语句块。
{函数体}//语法格式
例
{println()}
//Lambda表达式调用的格式
{函数体}()
例
{println()}()
可见,在Lambda表达式后添加“()”,便达到了调用该表达式的效果。
fun main() {
{
println("Lambda无参有返回")
}()
}
有参有返回
需要指定参数名称,参数类型可省略,函数体自动会校对。
Lambda表达式的箭头,用于指定参数或数据的指向。
//语法格式
{参数名: 参数类型, 参数名: 参数类型…… -> 函数体}
与无参有返回值的调用方式相同,只需要在表达式后方添加“()”,但因为是有参数的,所以要再括号中添加参数。
//调用格式
{参数名: 参数类型, 参数名: 参数类型…… -> 函数体}(参数1,参数2)
//方式1:函数体后方直接传入实参
fun main() {
val sum = { a: Int, b: Int ->
a + b
} (6,8)
println(sum)//运行结果:14
}
//方式2:声明后再调用
fun main() {
val sum = { a: Int, b: Int ->
a + b
}
println(sum(6, 8))//运行结果:14
}
Lambda表达式返回值
通过以上例子可知,Lambda表达式都是有返回值的,因此直接省略了返回值的类型和方法名。
那么是如何来声明返回值的类型和返回值的呢?
fun main() {
println("-----------------1------------------")
val result1 = {
println("输出语句1");
"字符串"
}()
println("返回值:$result1")
println("返回值类型:${result1.javaClass}")
println("-----------------2------------------")
val result2 = {
println("输出语句1")
println("输出语句2")
18
}()
println("返回值:$result2")
println("返回值类型:${result2.javaClass}")
println("-----------------3------------------")
val result3 = {
println("输出语句1")
println("输出语句2")
true
}()
println("返回值:$result3")
println("返回值类型:${result3.javaClass}")
}
![](https://i-blog.csdnimg.cn/blog_migrate/11ff376b5181836a848f25509cc8d721.png)
可见,不管方法体内的语句执行多少行,返回值的类型和返回值,都是由方法体中的最后一句决定的,因此再实际返回值后,不能编写任何内容。
若在实际返回值后编写内容,会发生什么呢?
fun main() {
var sum = { a: Int, b: Int ->
a + b
"trick"
}
println(sum(6, 8))//输出结果:trick
}
方法体内虽然是求ab之和,但返回值是“trick”,无法输出想要的结果。因此,不要将不是返回值的内容,放在最后一行。
高阶函数的使用
高阶函数和Lambda的关系是密不可分的。
Lambda表达式都是定义在方法内部,那么还可以在别的地方定义吗?答案是可以的。
Lambda表达式可以直接作为函数的实际返回值,这样的声明,在Kotlin中被称为高阶函数。如果一个函数,接受另一个函数作为参数,或者返回值的类型是另一个函数,那么这个函数就被称为高阶函数。
此时就涉及到了另一个概念:函数类型。在编程语言中有整型(Int)、布尔型(Boolean)等字段类型,而在kotlin中增加了一个函数类型的概念。如果我们将函数类型添加到一个函数的参数声明或者返回值声明当中,那么这个函数就是高阶函数了。
此处我自己的理解方式是:当kotlin这个独有的参数类型,被添加某个函数的括号内或引号后,那么他就是高阶函数。
fun gaojieFunction(括号内的接收参数) : 引号后的返回值 {...}
作为参数使用
Lambda表达式,除了定义在方法内部,还可以作为函数的实参。
假设当前有一个[1,50]的区间,要根据条件对元素进行筛选,但是条件众多,不可能把所有选择条件都写成方法,此时可使用Lambda表达式。
fun IntRange.pickNum(function: (Int) -> Boolean): List<Int> {
val resultList = mutableListOf<Int>() //可变集合
for (i in this) { //this指向区间范围IntRange
if (function(i)) { //判断传递过来的Lambda表达式返回值
resultList.add(i)
}
}
return resultList
}
fun main() {
val list = 1..20//区间范围
println("-------------能被5整除的数")
println(list.pickNum { x: Int -> x % 5 == 0 })//运行结果:[5, 10, 15, 20]
println("-------------能被10整除的数")
println(list.pickNum { x: Int -> x % 10 == 0 })//运行结果:[10, 20]
}
在上述代码中,首先声明了一个方法IntRange.pickNum(need: Int, function: (Int) -> Boolean),在这个方法中将Lambda表达式当作参数传递。
作为参数优化
当Lambda表达式作为函数参数使用时,有三种优化形式:省略小括号、将参数移动到小括号外面、使用it关键字。
省略小括号
若函数只有一个参数,且这个参数类型是一个函数类型,则在调用时可以去掉函数名后的小括号。
fun IntRange.pickNum(function: (Int) -> Boolean): List<Int> {
val resultList = mutableListOf<Int>()
for (i in this) {
if (function(i)) {
resultList.add(i)
}
}
return resultList
}
fun main() {
val list = 1..50
println("-------------能被10整除的数")
//省略前
println(list.pickNum({ x: Int -> x % 10 == 0 }))//输出结果:[10, 20, 30, 40, 50]
//省略后
println(list.pickNum { x: Int -> x % 10 == 0 })//输出结果:[10, 20, 30, 40, 50]
}
在pickNum方法中只有一个参数,且该参数类型为函数类型,此时可以省略括号。
将参数移动到小括号外面
如果一个函数有多个参数,但是最后一个参数类型是函数类型,那么在调用函数时,可以将最后一个参数从括号内移出,并去掉参数之间的逗号。
fun IntRange.pickNum(need: Int, function: (Int) -> Boolean): List<Int> {
val resultList = mutableListOf<Int>()
for (i in this) {
if (function(i)) {
resultList.add(i)
}
}
return resultList
}
fun main() {
val list = 1..50
println("-------------能被10整除的数")
//移出前
println(list.pickNum(1, { x: Int -> x % 10 == 0 }))
//移出后
println(list.pickNum(1) { x: Int -> x % 10 == 0 })
}
相比于之前的代码中,函数pickNum添加了一个参数。当使用该参数时,可以将Lambda表达式{ x: Int -> x % 10 == 0 }移动到括号外。
使用it关键字
无论函数包含多少个参数,如果其中有函数是参数类型,并且该函数满足只接收一个参数的要求,可以用it关键字代替函数的形参以及箭头。
fun IntRange.pickNum(function: (Int) -> Boolean): List<Int> {
val resultList = mutableListOf<Int>()
for (i in this) {
if (function(i)) {
resultList.add(i)
}
}
return resultList
}
fun main() {
val list = 1..50
println("-------------能被10整除的数")
//使用it之前
println(list.pickNum { x: Int -> x % 10 == 0 })
//使用it之后
println(list.pickNum { it % 10 == 0 })
}
作为返回值
enum class USER {
NORMAL, VIP
}
fun getPrice(userType: USER): (Double) -> Double {
if (userType == USER.NORMAL) {
return { it } //返回值是Lambda表达式
}
return { price -> 0.88 * price } //返回值是Lambda表达式
}
fun main() {
val normalUserPrice = getPrice(USER.NORMAL)(200.0)
println("普通用户价格:$normalUserPrice") //运行结果:普通用户价格:200.0
val vipPrice = getPrice(USER.VIP)(200.0)
println("超级会员价格:$vipPrice") //运行结果:超级会员价格:176.0
}
标准库中的高阶函数
Kotlin中存在大量已经定义好的、对于集合操作的高阶函数。
高阶函数操作集合
查找元素
find()方法
用于查找并返回第一个符合条件的元素,若无返回null。
fun main() {
val list = listOf(-2, -1, 0, 1, 2)
println("---------------find----------------")
println("find num > 0: ${list.find { it > 0 }}")//运行结果:find num > 0: 1
println("find num = 3: ${list.find { it == 3 }}")//运行结果:find num = 3: null
}
first()和last()方法
分别用于查找符合条件的第一个和最后一个元素,若无抛出错误。
fun main() {
val list = listOf(-2, -1, 0, 1, 2)
println("---------------first----------------")
println("find num > 0: ${list.first { it > 0 }}")
println("---------------last----------------")
println("find num > 0: ${list.last { it > 0 }}")
println("find num > 3: ${list.last { it > 3 }}")
}
![](https://i-blog.csdnimg.cn/blog_migrate/bb29f0d7b47c7d6fe1ff46108cfcf5da.png)
single()方法
用于在集合中查找符合条件的唯一元素,当集合中有多个或者没有该元素,抛出异常。
fun main() {
val list = listOf(-2, -1, 0, 1, 2)
println("find single > 1: ${list.single { it > 1 }}")
println("find single > -1: ${list.single { it > -1 }}")
}
![](https://i-blog.csdnimg.cn/blog_migrate/24742c41f89d784daf0f28b022cf9df8.png)
takeWhile()方法
查找多个满足条件的元素。
fun main() {
val list = listOf(-2, -1, 0, 1, 2)
println("大于-3的元素:${list.takeWhile { it > -3 }}")//运行结果:大于-3的元素:[-2, -1, 0, 1, 2]
println("大于0的元素:${list.takeWhile { it > 0 }}")//运行结果:大于0的元素:[]
println("小于0的元素:${list.takeWhile { it < 0 }}")//运行结果:小于0的元素:[-2, -1]
}
输出结果可看出这种方法存在局限,只有当集合中的第一个元素满足条件,才会继续向下查找。
filter()方法
与takeWhile方法较为类似,但没有takeWhile方法的局限性。
fun main() {
val list = listOf(-2, -1, 0, 1, 2)
println("大于-3的元素:${list.filter { it > -3 }}")//运行结果:大于-3的元素:[-2, -1, 0, 1, 2]
println("大于0的元素:${list.filter { it > 0 }}")//运行结果:大于0的元素:[1, 2]
println("小于0的元素:${list.filter { it < 0 }}")//运行结果:小于0的元素:[-2, -1]
}
count()方法
用于查找满足当前条件的元素个数。
fun main() {
val list = listOf(60, 80, 100, 120, 140)
println("查找大于100的元素个数:${list.count { it > 100 }}")//运行结果:2
println("查找小于60的元素个数:${list.count { it < 60 }}")//运行结果:0
}
比较元素
方法声明 | 功能描述 |
maxBy | 查找最大值 |
maxBy | 查找最大值 |
distinceBy | 去除重复的元素 |
fun main() {
val list = listOf(-2, 0, 0, 1, 1, 2)
println("----------查找最大值----------")
println(list.maxBy { it }) //运行结果:2
println("----------查找最小值----------")
println(list.minBy { it }) //运行结果:-2
println("----------集合去重----------")
println(list.distinctBy { it }) //运行结果:[-2, 0, 1, 2]
}
标准库的高阶函数
repeat()函数
用于重复执行某条语句,和循环语句非常相似。
fun main() {
println("第一个参数值为2时")
repeat(2,{ println("China")})
println("第一个参数值为1时")
repeat(1,{ println("China")})
println("第一个参数值为0时")
repeat(0,{ println("China")})
}
![](https://i-blog.csdnimg.cn/blog_migrate/f24f3603b30943599e09e6e357766dfd.png)
可见,在使用该方法时,若要确保能输出结果,第一个参数值要大于0。
T.run()函数
//在ArrayList集合中添加函数的一般操作
fun main() {
val list=ArrayList<Int>()
list.add(1)
list.add(2)
list.add(3)
println(list)//运行结果:[1, 2, 3]
}
//使用run函数后的ArrayList集合添加操作
fun main() {
val list=ArrayList<String>()
list.run {
this.add("土星")
add("天王星")
add("海王星")
}
println(list)//运行结果:[土星, 天王星, 海王星]
}
ArrayList集合添加数据时会有一个返回值,当数据添加成功时,返回true。
在run函数中添加是什么样的呢?
fun main() {
val list=ArrayList<String>()
val value=list.run {
add("金星")
size
}
println(value) //运行结果:1
println(list) //运行结果:[金星]
}
可见在run函数中返回值为函数体的最后一条语句。
fun main() {
val list = ArrayList<String>()
val value = list.run {
add("金星")
println("集合数据:$list")//运行结果:集合数据:[金星]
return
size
}
println(value)
println(list)
}
在run函数可以使用return来结束当前语句。可见在代码示例中,有打印“集合数据”这一行,但是最后的两行没有加入打印,这是因为在这之前已经return,会直接结束当前的方法以及方法外部的内容。
而这样直接结束main方法,显然是不符合逻辑的。
fun main() {
val list = ArrayList<String>()
val value = list.run {
add("金星")
println("集合数据:$list") //运行结果:集合数据:[金星]
return@run size
add("火星")
println("集合数据:$list")
}
println(value) //运行结果:1
println(list) //运行结果:[金星]
}
通过代码实例可见,当使用return@run可以结束当前的run函数,并且不会结束main方法。这就解决了使用return直接结束main方法的问题。并且在Standard类中的所有方法,都可以通过“return@方法名:的这种格式来结束当前方法。
内联函数(inline)
在Kotlin中,Lambda表达式会被编写成一个匿名类,这样每次调用的时候,就会创建出新的对象,造成额外的内存开销。为了解决这个问题,可以使用inline修饰符,被这个关键词修饰的Lambda函数被称为内联函数。
使用内联函数
内联函数可以降低程序的内存消耗,但不要内联一个复杂功能的函数,尤其在循环中。
inline fun <T> check(lock: Lock, body: () -> T): T {
lock.lock()
try {
return body()
} finally {
lock.unlock()
}
}
fun main() {
var l = ReentrantLock()
check(l) { println("这是内联函数方法体") }//运行结果:这是内联函数方法体
}
在示例中,inline关键字指定的check函数就是一个内联函数。
在调用内联函数check时,编译器会对”check(l) { println("这是内联函数方法体") }“这句代码进行优化,去掉方法名称以避免入栈出栈操作,直接执行方法体内的内容。
对于函数调用的入栈出栈,可以参考代码的断点调试。
而在调用内联函数时,代码被简化为如下:
lock.lock()
try {
return "这是内联函数方法体"//内联函数修改的地方
} finally {
lock.unlock()
}
在编译时,内联的写法会将函数的代码直接插入到函数调用的地方,而不是通过函数调用的方式进行执行。换句话说,编译器会将内联函数的代码复制到调用处,以消除函数调用的开销。
内联函数实际上在运行期间会增加代码量,但对可读性不影响,且提升了性能。
禁用内联函数(noinline)
当使用内联函数时,参数也会相应内联,因此便出现了一个问题:若参数有Lambda表达式时,Lambda表达式便不再是一个函数的对象,从而无法当作参数来传递。
通俗来说,内联避免了函数调用,直接把参数简单粗暴地插入在了内联函数内部,因此Lambda表达式不再存在参数传递。
为了解决这个问题,可使用noinline修饰符。
inline fun check(noinline function: (Int) -> Boolean) {
test(function)
}
fun test(function: (Int) -> Boolean) {
println("编译通过")
}
fun main() {
check { x: Int -> x == 2 }
}
作用域函数
作用域函数指的是kotlin标准库中Standard.kt中的函数,也包含Closeable.kt中的use函数,因为该函数与作用域函数十分相似。
let函数
let函数对于确定变量范围和处理可控类型是十分有用的。有以下三个主要使用场景:
- 限定变量作用域,使其仅在lambda中生效
- 仅当可空变量不为null时才执行一些代码
- 将一个可空对象转换为另一个可空对象
首先,当变量或对象只应该在代码的一小部分使用时,就可以划定其作用域。
//使用let函数划定作用域
val lines = File("rawdata.csv").bufferedReader().let {
val result = it.readLines()
it.close()
result
}
//bufferedReader和result变量在此处不可见
示例中,展示了一个读取文件的缓冲读取器。以此方式定义的缓冲读取器代码和let代码块中声明的所有变量,只在此块中可见。这避免了从这段代码块之外对读取器进行的不必要的访问。与if和when类似,let会返回lambda的最后一行中定义的值,在示例中,返回的即是result的值。
另外,let对于处理可空变量非常有用,因为当使用安全调用操作符调用可控对象时,只有当对象不为空时,才会执行let代码块,否则,let代码将会被忽略。
//使用let处理可空类型
val weather: Weather? = fetchWeatherOrNull()
weather?.let {
updateUi(weather.temperature)//只有当变量weather不为空的时候才会更新ui
}
示例中,使用一个可能返回null的网络调用来获取天气数据。如此对于一个可空变量结合一个安全调用符使用let时,kotlin编译器将会自动在let代码块中,将原本可空类型的变量weather,转换为不可空类型。这样一来,就可以在使用该变量时避免可空型校验。如果无法获取weather的值,那么let代码块将不会执行,ui自然也就不会更新。
apply函数
高阶函数apply两个主要使用场景:
- 封装对同一对象的多次调用
- 初始化对象
//apply函数的使用
countryToCapital.apply {
putifAbsent("India", "Delhi")//无前缀使用countryToCapital里的方法
putifAbsent("France", "Paris")
}
println(countryToCapital)
示例中,调用apply时无需再使用it,因为传入的lambda更像是对调用apply的对象(示例中是countryToCapital)执行扩展函数一样。这里也可以使用this.putIfAbsent,但这里的this前缀是可选的。
然而,apply最常用的用法是初始化对象。
//使用apply初始化对象
val container = Container().apply {
size = Dimension(1024, 800)
font = Font.decode("Arial-bold-22")
isVisible = true
}
示例中,使用了apply方法的特性,也就是他会首先运行lambda中的所有代码,然后返回最开始调用的对象。因此变量container包含了在lambda中的所有改变,这也使得apply能够使用链式操作。
//返回apply的值
countryToCapital.apply {…} //apply返回修改后的countryToCapital
.filter {…} //filter在apply返回的结果上运行
.map {…} //map在filter返回的结果上运行
使用这种结构,能够在代码中通过缩进明确的突出对同一个对象的操作,并且不需要为每个调用重复命名。
with函数
with函数的行为和apply函数基本相同,其主要用于以下两种场景:
- 封装对同一对象的多次调用
- 限制临时对象的使用范围
与apply不同的是,with把lambda最后一行最为返回值,而非调用他的对象。
//使用with来封装同一对象的多次调用
val countryToCapital = mutableMapOf("Germany" to "Berlin")
val countries = with(countryToCapital) {
putIfAbsent("England", "London")
putIfAbsent("Spain", "Madird")
keys //定义lambda的返回值
}
println(countryToCapital)//运行结果:{Germany=Berlin, England=London, Spain=Madrid}
println(countries)//运行结果:[Germany, England, Spain]
示例中,说明了with的返回值是我们传入lambda表达式的返回值,即lambda最后一行的返回值。代码中,最后一行的返回值是map类型的countries中的key(在示例中是countries.keys),所以这个with代码块返回的是所有map中的key值,并存储在countries中。
且与let类似,当with中传入一个创建的特定对象时,可以限制这个对象只能在该lambda表达式的范围中。
//使用with来限制范围
val essay = with(StringBuilder()) {//只在该lambda中stringbuilder是可被访问的
appendln("Intro")
appendln("Content")
appendln("Conclusion")
toString()
}
在示例的情况下,使用with要比apply更合适,因为我们更想获得lambda的结果(即essay的值),而不是StringBuilder返回的结果。类似上面代码中使用builder是最常见的说明with比apply更合适的例子。
run函数
run函数主要用于以下几个场景:
- 与let函数一样,作用于可空对象,但在内部使用this而不是it
- 能够立即执行
- 将变量的作用范围控制在lambda内部
- 将显示参数转换为接收器对象
首先,run可以看作是with和let的结合。修改countryToCapital如下所示:
fun main() {
val countryToCapital: MutableMap<String, String>?
= mutableMapOf("Germany" to "Berlin")
val countries = countryToCapital?.run {
putIfAbsent("Mexico", "Mexico City")
putIfAbsent("Germany", "Berlin")
keys
}
println(countryToCapital)//运行结果:{Germany=Berlin, Mexico=Mexico City}
println(countries)//运行结果:[Germany, Mexico]
}
示例中的代码与“使用with来封装同一对象的多次调用”的目的是一样的,唯一的不同是,如果该map为空,则会跳过lambda中代码的执行。
其次,可以用run来立即执行功能,也就是可以立即执行一个给定的lambda。
fun main() {
run {
println("Running lambda")
val a = 11 * 13
}//运行结果:Running lambda
}
示例同时暗示了下一个例子,即将临时变量的作用域最小化在lambda表达式范围内。
val success = run {
val username = getUsername()//只在此lambda中可见
val password = getPassword()//只在此lambda中可见
validate(username, password)
}
示例中,username和password只在他们真正需要的地方可被访问,这是一种减少变量访问范围,来避免不必要的访问权限的方法。
最后,run可以用来将显示的变量转换为接收器对象。
fun renderUsername(user: User) = user.run {
val premium = if (paid) "(Premium)" else ""
val displayName = "$username$premium"
println(displayName)
}
示例中,可以直接访问User类中的所有成员(或者通过this)。如果参数名很长,又必须在函数内部多次访问它,那么调用run函数将非常有用。
also函数
also是Standard.kt中的最后一个高阶函数,他主要有两个使用场景:
- 执行验证或者记录等辅助操作
- 拦截功能链
//使用also进行验证
val user = fetchUser().also {
requireNotNull(it)
require(it!!.mothlyFee > 0)
}
示例中,首先调用fetchUser方法可能返回null,然后lambda表达式被执行,并且只有该方法运行后才会赋值给user。
此外,在执行类似日志打印或者其他相对不重要的操作时,只有在链式调用中才会变得显而易见。
users.filter { it.age > 21 }
.also { println("${it.size} adult users found.") }
.map { it.monthlyFee }
示例中,在链外面没有直接使用also函数的需求,因为可以在下一行执行日志记录或者其他操作。但是在这里,我们可以在不打断链调用的情况下,使用also函数来截取中间结果。
use函数
use函数不是Standard.kt的内容,但也具有相似的结构和优点:他能保证调用者在执行完给定的操作之后关闭资源。因此,use函数仅仅为Closeable的子类所定义使用,如Reader、Writer或Socket。
//使用use对Closeable操作
val lines = File("rawdata.csv").bufferedReader().use { it.readLines() }
示例中,由于在use代码块的结尾可以自动关闭bufferedReader,所以没必要将结果存储在临时变量中,且可以将lambda缩减到只有一行。use除了可以保证close被调用,其他的用法和let类似。在其内部实现中,use函数有一个finally代码块来保证Closeable对象最终能够被关闭。
组合高阶函数
高阶函数非常好用,一定程度上是因为可以使用链式操作。
val sql = SqlQuery().apply { //apply初始化变量
append("INSERT INTO user (username, age, paid) VALUE (?, ?, ?)")
bind("johndor")
bind(42)
bind(true)
}.also { //also拦截功能链,来进行辅助操作
println("Initialized SQL query: $it")
}.run { //run执行操作
DbConnection().execute(this)
}
正如前文推荐的那样,示例中使用apply来初始化SqlQuery对象,然后截取链并使用also来记录查询的结果,接着使用run来执行查询操作。通过示例中的方式,限制了SqlQuery和DbConnection对象的作用域,他们在这个函数链的外部是没有访问权限的。
除此之外,也可以将范围操作函数与其他的高阶函数结合使用。
//组合使用多个高阶函数
val authors = authorsToBooks.apply {
putIfAbsent("Martin Fowler", listOf("Patterns of Enterprise Application Arch"))
}.filter {
it.value.isNotEmpty()
}.also {
println("Authors with books: ${it.keys}")
}.map {
it.key
}
示例中,首先使用apply来增加一个作者,若不存在则添加(putIfAbsent),其次过滤出书单不为空的所有作者(value.isNotEmpty),并使用also来输出所有符合条件的作者,最后使用map将该map转换为一个作者列表。
带接收者的lambda
高阶函数apply、with和run有一个潜在的特性,即当引用对象时,使用this而非it,这种特性叫做带接收者的lambda。这里的接收者指的是调用该函数的对象,比如在代码“组合使用多个高阶函数”中使用apply的authorsToBooks变量。在lambda表达式中传入的参数与接收者相关联,这使得我们可以直接访问接收者类中的成员变量。换句话说,我们写的lambda表达式就像是对该接收者的扩展函数,而实际上lambda的语法也与扩展函数很相似。
//let和run的签名
fun <T,R> T.let(block: (T) -> R): block(this)
fun <T,R> T.run(block: T.() -> R): block()
这两个签名的唯一不同之处在于lambda的输出参数。let和run都被定义为泛型T的扩展函数,但是,let接受T作为参数的方法(block),当let被调用时,会在类型为T的对象上调用该方法。因此,在lambda内部,该参数是可以作为it被访问到的。
另一方面,run函数传入的lambda使用T.()->R定义了T作为其接收者。这样能够使lambda成为T的扩展函数,这意味着可以通过this来访问T的所有成员变量,使得在调用run方法的实现时,可以使用this.block(),或者简单的block()。
这是区分这五个作用于函数的一种方法,实际上,有三种不同的维度区分它们:
- 作用于函数的参数是带接收值的lambda或者“正常”参数的lambda——分别在lambda内部使用this或者it
- 作用于函数返回的是调用对象,还是返回lambda的返回值
- 作用域函数本身是一个扩展函数,还是接受一个参数(with是唯一需要接受一个显示参数的函数)