Skip to content

Kotlin 函数进阶

在 Kotlin 中,函数被视为“头等公民”,这意味着:

  • 函数可以存储在变量和数据结构
  • 函数可以作为参数和返回值供其他函数使用

同时,Kotlin 也提供了非常多的函数特性,如 Lambda 表达式、扩展函数等,以下是对这些特性的简单介绍

高阶函数

函数的类型由参数类型和返回值类型组成,格式为 (参数名称: 参数类型, ...) -> 返回值类型,其中参数名称是可选的,通常用于表明参数含义并提高可读性

将函数作为参数或返回值的函数被称为高阶函数,例如 forEachmapfilter 等都接收一个函数作为参数,也比如下方的函数:

kotlin
// 接收一个整数数组和一个参数及返回值都是 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 表达式由于语法简洁、可直接作为表达式使用,因此常作为高阶函数的参数进行传递。其完整语法形式如下:

kotlin
// 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 表达式进行逐步简写可得到:

kotlin
(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 控制返回行为

声明匿名函数的语法形式与常规函数相同,只是缺省了函数名:

kotlin
val sum: (Int, Int) -> Int = fun(x: Int, y: Int): Int {
    return x + y
}

返回行为区别

由于 return 语句总是从由 fun 关键字定义的函数中返回,因此 Lambda 表达式和匿名函数内的返回行为是不同的:

  • Lambda 表达式中的 return 关键字将从包含它的外层函数中返回,该行为称为非局部返回
  • 匿名函数中的 return 关键字将从匿名函数自身中返回

扩展函数

扩展函数是一种特殊的函数,它允许为已有的类添加新的方法,而无需修改类的定义。声明扩展函数时,需要使用接收者类型(被扩展的类型)作为函数名称的前缀,声明后即可在接收者(该类型的实例)上调用该函数:

kotlin
// String 类型的扩展函数, 用于判断字符串是否为回文
fun String.isPalindrome(): Boolean {
    // 扩展函数内的 this 即为调用该函数的接收者
    return this == this.reversed()
}

fun main() {
    println("hello".isPalindrome())
    println("level".isPalindrome())
}

可空接收者

接收者类型允许为可空类型,此时即使接收者为 null 也可以正常调用该扩展函数:

kotlin
// 可空类型 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 引用接收者:

kotlin
val isPalindrome: String.() -> Boolean = { this == this.reversed() }
println("level".isPalindrome())

函数引用

当需要使用已声明的函数时,可以直接对其进行引用,而无需重新创建相同功能的函数字面量,函数引用的语法主要有以下几种形式:

  • 顶层 / 局部函数引用,如 ::println
  • 成员 / 扩展函数引用,如 String::substring
  • 构造函数引用,如 ::MyClass
  • 绑定函数引用,如 myInstance::myFunction

直接对成员 / 扩展函数的引用进行调用时,需要将所需类型的实例作为第一个参数传入,充当函数执行所需的上下文:

kotlin
val substringRef: (String, Int, Int) -> String = String::substring
val result = substringRef("Hello, Kotlin!", 7, 13) // "Kotlin"

上述操作可行原因

在 Kotlin 中,一个类型为 (A, B) -> CA.(B) -> C函数非字面量可以被视为同时满足这两种函数类型。这意味着此处 String::substring 对应的类型 String.(Int, Int) -> String 可以被当作类型 (String, Int, Int) -> String 来进行赋值与调用

作用域函数

Kotlin 提供了五种作用域函数,用于在目标对象的上下文中执行代码以便快捷操作对象,它们分别是 letrunalsoapplywith,这些作用域函数的区别如下:

函数返回值对象引用为扩展函数
let函数结果it
run函数结果this
also目标对象it
apply目标对象this
with函数结果this
各作用域函数的签名及区别

五种作用域函数的签名依次如下:

kotlin
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

可根据接收函数的返回值类型,区分作用域函数的返回值:

  • letrun 接收的函数有返回值,它们会返回传入函数的执行结果
  • alsoapply 接收的函数无返回值,它们会重新返回目标对象

可根据接收函数中目标对象的位置,区分作用域函数的对象引用:

  • letalso 接收常规函数,目标对象作为参数,函数体中 it 为对目标对象的引用
  • runapply 接收扩展函数,目标对象作为接收者,函数体中 this 为对目标对象的引用

在所有作用域函数中,只有 with 不是扩展函数,而是将目标对象作为参数的顶层函数,除此以外,它和 run 的功能完全相同

合理使用这些作用域函数,通过更改上下文环境以及控制代码结构,可以方便的对目标对象的相关操作进行组合、附加或隔离,也更容易写出更清晰易维护的代码。下面是作用域函数的几个经典使用场景:

kotlin
// 可用 let 与空值运算符相结合, 简化对非空对象的操作
val user: User? = getUser()
val result = user?.let {
    "Fetched user: ${it.name}, ${it.age} years old"
} ?: "Default user: Unknown, 0 years old"
kotlin
// 当对目标对象进行多个操作才能获得期望结果时
// 可用 run / with 将相关操作放入同一作用域中
val greetingMessage = StringBuilder().run {
    // 该作用域中的变量被隔离, 不会污染外部环境
    val username = getUsername()
    append("Hello,")
    append("$username!")
    toString()
}
kotlin
// 可用 also 在不中断链式调用的情况下, 为目标对象执行额外的操作
fun generateRandomList(size: Int): List<Double> {
    return List(size) { Random.nextDouble(10.0) }
        .also { println("生成的随机列表: $it") }
        .sorted()
        .also { println("排序后的列表: $it") }
}
kotlin
// 可用 apply 对目标对象进行多个属性配置或方法调用, 还可省略 this 精简代码
val user = User().apply {
    name = "Alice"
    age = 18
    introduce()
}

对于作用域函数的选择,可以参考以下约定:

  • 函数体使用外部变量和函数,或目标对象作为参数传递时,选用对象引用为 it 的作用域函数
  • 函数体主要为目标对象进行属性赋值和方法调用时,选用对象引用为 this 的作用域函数
  • 当需要通过目标对象获取相关的期望结果时,选用返回函数结果的作用域函数
  • 当需要不破坏原有的链式调用结构时,选用返回目标对象的作用域函数

中缀函数

中缀函数在之前就已经出现过,例如区间遍历中的 downTostep 都是中缀函数,其特点是可以使用中缀表示法进行调用,即忽略点和括号的调用形式:

kotlin
for (i in 100 downTo 1 step 2) { println(i) }

要声明一个中缀函数,需要使用 infix 关键字标记该函数,但函数也要同时满足以下要求:

  • 必须是成员函数或扩展函数
  • 必须只有一个参数且不能有默认值

中缀表示法中,函数左侧为接收者,在函数体中使用 this 进行引用,右侧则为函数的唯一参数。例如 downTo 作为 Int 的扩展函数,其函数声明如下:

kotlin
public infix fun Int.downTo(to: Int): IntProgression {
    return IntProgression.fromClosedRange(this, to, -1)
}

可变参数

在函数声明的参数中,可以使用 vararg 关键字对至多一个参数标记为可变参数,此时该参数被视为数组,可接收任意数量的参数:

kotlin
fun printNumbers(vararg numbers: Int) {
    // 此时 numbers 被视为一个 Int 类型的数组
    for (number: Int in numbers) {
        println(number)
    }
}

在传递可变参数时,可以直接传入多个参数,也可以使用数组搭配展开运算符 * 传入数组内容:

kotlin
// 直接传入多个参数
printNumbers(1, 2, 3)

// 使用已有的数组搭配展开运算符 * 传入多个参数
val arr = intArrayOf(4, 5, 6)
printNumbers(*arr)

通常将可变参数放在最后,否则可变参数的后续参数需要使用具名参数的方式进行传递:

kotlin
fun printNumbersWithOffset(vararg numbers: Int, offset: Int) {
    numbers.forEach { println(it + offset) }
}

fun main() {
    // 可变参数的后续参数只能使用具名参数的方式进行传递
    printNumbersWithOffset(1, 2, 3, offset = 100)
}