Kotlin 函数进阶
在 Kotlin 中,函数被视为“头等公民”,这意味着:
- 函数可以存储在变量和数据结构中
- 函数可以作为参数和返回值供其他函数使用
同时,Kotlin 也提供了非常多的函数特性,如 Lambda 表达式、扩展函数等,以下是对这些特性的简单介绍
高阶函数
函数的类型由参数类型和返回值类型组成,格式为 (参数名称: 参数类型, ...) -> 返回值类型
,其中参数名称是可选的,通常用于表明参数含义并提高可读性
将函数作为参数或返回值的函数被称为高阶函数,例如 forEach
、map
、filter
等都接收一个函数作为参数,也比如下方的函数:
// 接收一个整数数组和一个参数及返回值都是 Int 类型的函数作为参数
fun applyOperation(numbers: Array<Int>, operation: (Int) -> Int) {
for (number in numbers) {
val result = operation(number)
println("Operation result for $number is: $result")
}
}
函数类型无返回值的情况
与常规函数的声明不同,当函数类型无返回值时,为了保持函数类型语法的一致和清晰,返回值类型不可省略,需显式的声明为 Unit
,如 () -> Unit
当调用高阶函数时,需要为函数类型的参数传入对应的函数实例,获取函数实例的方式如下:
- 使用函数字面量,即未声明就直接作为表达式传递的函数:
- 使用函数的非字面量,即已声明的函数:
- 函数引用
- 其他高阶函数返回的函数实例
Lambda 表达式
Kotlin 中的 Lambda 表达式由于语法简洁、可直接作为表达式使用,因此常作为高阶函数的参数进行传递。其完整语法形式如下:
// Lambda 表达式语法: { 参数名称: 可选的参数类型, ... -> 函数体 }
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
如果 Lambda 表达式的返回值类型不是 Unit
,则函数体中的最后一个表达式将作为 Lambda 表达式的返回值(如上面的 x + y
是函数体中的唯一表达式,也是该 Lambda 表达式的返回值)
Lambda 表达式也有很多简写规则:
- 拖尾 Lambda: 当最后的参数是函数类型,可以将 Lambda 表达式放在函数调用的括号之外
- 省略调用括号: 当 Lambda 表达式是函数的唯一参数时,可以省略函数调用的括号
- 隐式单参数: 当 Lambda 表达式只有一个参数时,可以省略参数声明,并使用
it
代替该参数
依照上述规则对原始形式的 Lambda 表达式进行逐步简写可得到:
(1..10).forEach({ i -> println(i) }) // 原始形式
(1..10).forEach() { i -> println(i) } // 拖尾 Lambda
(1..10).forEach { i -> println(i) } // 省略调用括号
(1..10).forEach { println(it) } // 隐式单参数
匿名函数
大多数情况下,Lambda 表达式可以完全替代匿名函数,但在某些情况下匿名函数更加灵活:
- Lambda 表达式无法指定返回值类型,只能依靠类型推断,而匿名函数可显式指定
- Lambda 表达式只返回最后一个表达式的值,而匿名函数内可使用
return
控制返回行为
声明匿名函数的语法形式与常规函数相同,只是缺省了函数名:
val sum: (Int, Int) -> Int = fun(x: Int, y: Int): Int {
return x + y
}
返回行为区别
由于 return
语句总是从由 fun
关键字定义的函数中返回,因此 Lambda 表达式和匿名函数内的返回行为是不同的:
- Lambda 表达式中的
return
关键字将从包含它的外层函数中返回,该行为称为非局部返回 - 匿名函数中的
return
关键字将从匿名函数自身中返回
扩展函数
扩展函数是一种特殊的函数,它允许为已有的类添加新的方法,而无需修改类的定义。声明扩展函数时,需要使用接收者类型(被扩展的类型)作为函数名称的前缀,声明后即可在接收者(该类型的实例)上调用该函数:
// String 类型的扩展函数, 用于判断字符串是否为回文
fun String.isPalindrome(): Boolean {
// 扩展函数内的 this 即为调用该函数的接收者
return this == this.reversed()
}
fun main() {
println("hello".isPalindrome())
println("level".isPalindrome())
}
可空接收者
接收者类型允许为可空类型,此时即使接收者为 null
也可以正常调用该扩展函数:
// 可空类型 String? 的扩展函数
fun String?.isPalindrome(): Boolean {
if (this == null) return false
return this == this.reversed()
}
fun main() {
println("level".isPalindrome())
println(null.isPalindrome())
}
扩展函数类型
要声明扩展函数的类型,只需在参数列表前加上接收者类型,如 String.() -> Boolean
。在扩展函数类型的函数字面量中,同样可以使用 this
引用接收者:
val isPalindrome: String.() -> Boolean = { this == this.reversed() }
println("level".isPalindrome())
函数引用
当需要使用已声明的函数时,可以直接对其进行引用,而无需重新创建相同功能的函数字面量,函数引用的语法主要有以下几种形式:
- 顶层 / 局部函数引用,如
::println
- 成员 / 扩展函数引用,如
String::substring
- 构造函数引用,如
::MyClass
- 绑定函数引用,如
myInstance::myFunction
直接对成员 / 扩展函数的引用进行调用时,需要将所需类型的实例作为第一个参数传入,充当函数执行所需的上下文:
val substringRef: (String, Int, Int) -> String = String::substring
val result = substringRef("Hello, Kotlin!", 7, 13) // "Kotlin"
上述操作可行原因
在 Kotlin 中,一个类型为 (A, B) -> C
或 A.(B) -> C
的函数非字面量可以被视为同时满足这两种函数类型。这意味着此处 String::substring
对应的类型 String.(Int, Int) -> String
可以被当作类型 (String, Int, Int) -> String
来进行赋值与调用
作用域函数
Kotlin 提供了五种作用域函数,用于在目标对象的上下文中执行代码以便快捷操作对象,它们分别是 let
、run
、also
、apply
、with
,这些作用域函数的区别如下:
函数 | 返回值 | 对象引用 | 为扩展函数 |
---|---|---|---|
let | 函数结果 | it | ✅ |
run | 函数结果 | this | ✅ |
also | 目标对象 | it | ✅ |
apply | 目标对象 | this | ✅ |
with | 函数结果 | this | ❌ |
各作用域函数的签名及区别
五种作用域函数的签名依次如下:
fun <T, R> T.let(block: (T) -> R): R
fun <T, R> T.run(block: T.() -> R): R
fun <T> T.also(block: (T) -> Unit): T
fun <T> T.apply(block: T.() -> Unit): T
fun <T, R> with(receiver: T, block: T.() -> R): R
可根据接收函数的返回值类型,区分作用域函数的返回值:
let
、run
接收的函数有返回值,它们会返回传入函数的执行结果also
、apply
接收的函数无返回值,它们会重新返回目标对象
可根据接收函数中目标对象的位置,区分作用域函数的对象引用:
let
、also
接收常规函数,目标对象作为参数,函数体中it
为对目标对象的引用run
、apply
接收扩展函数,目标对象作为接收者,函数体中this
为对目标对象的引用
在所有作用域函数中,只有 with
不是扩展函数,而是将目标对象作为参数的顶层函数,除此以外,它和 run
的功能完全相同
合理使用这些作用域函数,通过更改上下文环境以及控制代码结构,可以方便的对目标对象的相关操作进行组合、附加或隔离,也更容易写出更清晰易维护的代码。下面是作用域函数的几个经典使用场景:
// 可用 let 与空值运算符相结合, 简化对非空对象的操作
val user: User? = getUser()
val result = user?.let {
"Fetched user: ${it.name}, ${it.age} years old"
} ?: "Default user: Unknown, 0 years old"
// 当对目标对象进行多个操作才能获得期望结果时
// 可用 run / with 将相关操作放入同一作用域中
val greetingMessage = StringBuilder().run {
// 该作用域中的变量被隔离, 不会污染外部环境
val username = getUsername()
append("Hello,")
append("$username!")
toString()
}
// 可用 also 在不中断链式调用的情况下, 为目标对象执行额外的操作
fun generateRandomList(size: Int): List<Double> {
return List(size) { Random.nextDouble(10.0) }
.also { println("生成的随机列表: $it") }
.sorted()
.also { println("排序后的列表: $it") }
}
// 可用 apply 对目标对象进行多个属性配置或方法调用, 还可省略 this 精简代码
val user = User().apply {
name = "Alice"
age = 18
introduce()
}
对于作用域函数的选择,可以参考以下约定:
- 函数体使用外部变量和函数,或目标对象作为参数传递时,选用对象引用为
it
的作用域函数 - 函数体主要为目标对象进行属性赋值和方法调用时,选用对象引用为
this
的作用域函数 - 当需要通过目标对象获取相关的期望结果时,选用返回函数结果的作用域函数
- 当需要不破坏原有的链式调用结构时,选用返回目标对象的作用域函数
中缀函数
中缀函数在之前就已经出现过,例如区间遍历中的 downTo
和 step
都是中缀函数,其特点是可以使用中缀表示法进行调用,即忽略点和括号的调用形式:
for (i in 100 downTo 1 step 2) { println(i) }
要声明一个中缀函数,需要使用 infix
关键字标记该函数,但函数也要同时满足以下要求:
- 必须是成员函数或扩展函数
- 必须只有一个参数且不能有默认值
中缀表示法中,函数左侧为接收者,在函数体中使用 this
进行引用,右侧则为函数的唯一参数。例如 downTo
作为 Int
的扩展函数,其函数声明如下:
public infix fun Int.downTo(to: Int): IntProgression {
return IntProgression.fromClosedRange(this, to, -1)
}
可变参数
在函数声明的参数中,可以使用 vararg
关键字对至多一个参数标记为可变参数,此时该参数被视为数组,可接收任意数量的参数:
fun printNumbers(vararg numbers: Int) {
// 此时 numbers 被视为一个 Int 类型的数组
for (number: Int in numbers) {
println(number)
}
}
在传递可变参数时,可以直接传入多个参数,也可以使用数组搭配展开运算符 *
传入数组内容:
// 直接传入多个参数
printNumbers(1, 2, 3)
// 使用已有的数组搭配展开运算符 * 传入多个参数
val arr = intArrayOf(4, 5, 6)
printNumbers(*arr)
通常将可变参数放在最后,否则可变参数的后续参数需要使用具名参数的方式进行传递:
fun printNumbersWithOffset(vararg numbers: Int, offset: Int) {
numbers.forEach { println(it + offset) }
}
fun main() {
// 可变参数的后续参数只能使用具名参数的方式进行传递
printNumbersWithOffset(1, 2, 3, offset = 100)
}