在 Kotlin 中使用函数类型和 lambda 表达式

1. 简介

在此 Codelab 中,您将了解函数类型、函数类型的用法以及特定于 lambda 表达式的语法。

在 Kotlin 中,函数被视为一级结构。这意味着函数可以被视为数据类型。您可以将函数存储在变量中,将函数作为参数传递到其他函数,以及从其他函数返回函数。

与您可以使用字面量值表示的其他数据类型(例如值为 10Int 类型和值为 "Hello"String 类型)一样,您也可以声明函数字面量(称为 lambda 表达式,也可简称为 lambda)。在 Android 开发中,您会广泛使用 lambda 表达式;在 Kotlin 编程中,lambda 表达式的使用就更广泛了。

前提条件

  • 熟悉 Kotlin 编程,包括函数、if/else 语句和可为 null 性

学习内容

  • 如何使用 lambda 语法定义函数。
  • 如何将函数存储在变量中。
  • 如何将函数作为参数传递到其他函数。
  • 如何从其他函数返回函数。
  • 如何使用可为 null 的函数类型。
  • 如何使 lambda 表达式更简洁。
  • 什么是高阶函数。
  • 如何使用 repeat() 函数。

所需条件

  • 一个能够访问 Kotlin 园地的网络浏览器

2. 观看配套代码演示视频(可选)

如果您想要观看某位课程讲师完成此 Codelab 的过程,请播放以下视频。

建议将视频全屏展开(使用视频一角的 此符号突出显示了方形的 4 个角,表示全屏模式。 图标),以便更清楚地查看 Kotlin 园地和相关代码。

这是可选步骤。您也可以跳过视频,立即开始按照此 Codelab 中的说明操作。

3. 将函数存储在变量中

到目前为止,您已经学习了如何使用 fun 关键字来声明函数。使用 fun 关键字声明的函数可供调用,调用后,函数正文中的代码就会执行。

作为一种一级结构,函数也属于数据类型,因此,您可以将函数存储在变量中、将函数传递到函数,以及从函数返回函数。或许,您希望在运行时更改应用中某一部分的行为,或嵌入可组合函数以构建布局,就像您在之前的 Codelab 中所做的那样。这一切都可通过 lambda 表达式来实现。

您可以参照一些 trick-or-treating(不给糖就捣蛋)代码来理解该过程的实际运作。“不给糖就捣蛋”是指许多国家/地区过万圣节时的传统习俗:万圣节期间,孩子们会穿着万圣节的服饰挨家挨户地说“不给糖就捣蛋”,他们通常能换到糖果。

将函数存储在变量中:

  1. 访问 Kotlin 园地
  2. main() 函数后面,定义一个不带参数和返回值且输出 "No treats!"trick() 函数。其语法与您在之前的 Codelab 中看到的其他函数的语法相同。
fun main() {

}

fun trick() {
    println("No treats!")
}
  1. main() 函数的正文中,创建一个名为 trickFunction 的变量,并将其设置为与 trick 相等。请勿在 trick 后添加圆括号,因为您是想将函数存储在变量中,而不是调用函数。
fun main() {
    val trickFunction = trick
}

fun trick() {
    println("No treats!")
}
  1. 运行您的代码。代码会产生错误,因为 Kotlin 编译器会将 trick 识别为 trick() 函数的名称,但它想让您调用该函数,而不是将其分配给变量。
Function invocation 'trick()' expected

您试图将 trick 存储在 trickFunction 变量中。不过,如需将函数作为值引用,您需要使用函数引用运算符 (::)。语法如下图所示:

a9a9bfa88485ec67.png

  1. 如需将函数作为值引用,请将 trickFunction 重新分配给 ::trick
fun main() {
    val trickFunction = ::trick
}

fun trick() {
    println("No treats!")
}
  1. 运行代码,验证有没有其他错误。您会看到一条警告,提示您 trickFunction 从未使用,但此问题会在下一部分中修复。

使用 lambda 表达式重新定义函数

lambda 表达式提供了简洁的语法来定义函数,无需使用 fun 关键字。您可以直接将 lambda 表达式存储在变量中,无需对其他函数进行函数引用。

在赋值运算符 (=) 前面,您要添加 valvar 关键字,后跟变量名称,以供您在调用函数时使用。赋值运算符 (=) 后面是 lambda 表达式,它由一对大括号构成,而大括号中的内容则构成函数正文。语法如下图所示:

5e25af769cc200bc.png

使用 lambda 表达式定义函数时,您有一个引用该函数的变量。您还可以像对待任何其他类型一样,将其值分配给其他变量,并使用新变量的名称调用该函数。

更新代码以使用 lambda 表达式:

  1. 使用 lambda 表达式重写 trick() 函数。现在,名称 trick 将引用变量的名称。现在,大括号中的函数正文是 lambda 表达式。
fun main() {
    val trickFunction = ::trick
}

val trick = {
    println("No treats!")
}
  1. main() 函数中,移除函数引用运算符 (::),因为 trick 现在引用的是变量,而不是函数名称。
fun main() {
    val trickFunction = trick
}

val trick = {
    println("No treats!")
}
  1. 运行您的代码。此时没有任何错误,您可以在不使用函数引用运算符 (::) 的情况下引用 trick() 函数。也没有输出,因为您尚未调用函数。
  2. main() 函数中,调用 trick() 函数,但这次要包含圆括号,就像您在调用任何其他函数时所做的那样。
fun main() {
    val trickFunction = trick
    trick()
}

val trick = {
    println("No treats!")
}
  1. 运行您的代码。系统会执行 lambda 表达式的正文。
No treats!
  1. main() 函数中,将 trickFunction 变量视为函数进行调用。
fun main() {
    val trickFunction = trick
    trick()
    trickFunction()
}

val trick = {
    println("No treats!")
}
  1. 运行您的代码。系统会调用函数两次,一次针对 trick() 函数调用,第二次针对 trickFunction() 函数调用。
No treats!
No treats!

借助 lambda 表达式,您可以创建用于存储函数的变量,像调用函数一样调用这些变量,并将其存储在其他可以像函数一样调用的变量中。

4. 将函数用作数据类型

在之前的 Codelab 中,您已了解到 Kotlin 具有类型推断。声明变量时,您通常不需要明确指定类型。在前面的示例中,Kotlin 编译器能够推断出 trick 的值是函数。不过,如果要指定函数参数的类型或返回值类型,则需要了解用于表达函数类型的语法。函数类型由一组圆括号组成,其中包含可选的参数列表、-> 符号和返回值类型。语法如下图所示:

5608ac5e471b424b.png

您之前声明的 trick 变量的数据类型为 () -> Unit。圆括号为空,因为函数没有任何参数。返回值类型为 Unit,因为函数不返回任何内容。如果您的参数接受两个 Int 参数并返回 Int,则其数据类型为 (Int, Int) -> Int

使用明确指定函数类型的 lambda 表达式声明另一个函数:

  1. trick 变量后面,声明一个名为 treat 的变量,使其与正文输出 "Have a treat!" 的 lambda 表达式相等。
val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}
  1. treat 变量的数据类型指定为 () -> Unit
val treat: () -> Unit = {
    println("Have a treat!")
}
  1. main() 函数中,调用 treat() 函数。
fun main() {
    val trickFunction = trick
    trick()
    trickFunction()
    treat()
}
  1. 运行代码。treat() 函数的行为类似于 trick() 函数。这两个变量具有相同的数据类型,尽管只有 treat 变量明确声明数据类型。
No treats!
No treats!
Have a treat!

将函数用作返回值类型

函数是一种数据类型,因此您可以像使用任何其他数据类型一样使用函数。您甚至可以从其他函数返回函数。语法如下图所示:

f16dd6ca0c1588f5.png

创建一个可返回函数的函数。

  1. main() 函数中删除代码。
fun main() {

}
  1. main() 函数后面,定义一个接受 Boolean 类型的 isTrick 参数的 trickOrTreat() 函数。
fun main() {

}

fun trickOrTreat(isTrick: Boolean): () -> Unit {
}

val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}
  1. trickOrTreat() 函数的正文中,添加一个 if 语句,使其在 isTricktrue 时返回 trick() 函数,并在 isTrick 为 false 时返回 treat() 函数。
fun trickOrTreat(isTrick: Boolean): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        return treat
    }
}
  1. main() 函数中,创建一个名为 treatFunction 的变量,并将其分配给调用 trickOrTreat() 的结果,以为 isTrick 参数传入 false。然后,创建第二个变量(名为 trickFunction),并将其分配给调用 trickOrTreat() 的结果,这次要为 isTrick 参数传入 true
fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
}
  1. 调用 treatFunction(),然后调用下一代码行中的 trickFunction()
fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. 运行您的代码。您应该会看到每个函数的输出。即使您没有直接调用 trick()treat() 函数,您仍然可以调用它们,因为您存储了每次调用 trickOrTreat() 函数时的返回值,并使用 trickFunctiontreatFunction 变量调用了相关函数。
Have a treat!
No treats!

现在,您已经了解了函数如何返回其他函数。您还可以将一个函数作为参数传递到另一个函数。也许您需要为 trickOrTreat() 函数提供一些自定义行为,以执行除返回这两个字符串之一以外的其他操作。如果一个函数接受另一个函数作为参数,那么您每次调用该函数时,都可以传入一个不同的函数。

将一个函数作为参数传递到另一个函数

在世界上某些过万圣节的地区,孩子们会收到零钱(而非糖果),或者既能收到零钱,也能收到糖果。您将修改 trickOrTreat() 函数,以允许提供函数代表的其他招待内容作为参数。

trickOrTreat() 用作参数的函数也需要接受自己的参数。声明函数类型时,参数不会带有标签。您只需指定各个参数的数据类型(以英文逗号隔开)即可。语法如下图所示:

8372d3b83d539fac.png

当您为接受参数的函数编写 lambda 表达式时,系统会按参数出现的先后顺序为参数命名。参数名称列在左大括号后面,各名称之间以英文逗号隔开。箭头 (->) 将参数名称与函数正文隔开。语法如下图所示:

938d2adf25172873.png

trickOrTreat() 函数更新为接受函数作为参数:

  1. isTrick 参数后面,添加类型为 (Int) -> StringextraTreat 参数。
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
  1. else 代码块中,在 return 语句前面调用 println(),以传入对 extraTreat() 函数的调用。将 5 传入对 extraTreat() 的调用。
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        println(extraTreat(5))
        return treat
    }
}
  1. 现在,当您调用 trickOrTreat() 函数时,需要使用 lambda 表达式定义一个函数并为 extraTreat 参数传入该函数。在 main() 函数中,在对 trickOrTreat() 函数的调用前面添加一个 coins() 函数。coins() 函数为 Int 参数指定名称 quantity 并返回 String。您可能会发现这里没有 return 关键字,lambda 表达式中无法使用该关键字。相反,函数中最后一个表达式的结果将成为返回值。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. coins() 函数后面,添加一个 cupcake() 函数,如下所示。将 Int 参数命名为 quantity,并使用 -> 运算符将其与函数正文隔开。现在,您可以将 coins()cupcake() 函数传入 trickOrTreat() 函数。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val cupcake: (Int) -> String = { quantity ->
        "Have a cupcake!"
    }

    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}
  1. cupcake() 函数中,移除 quantity 参数和 -> 符号。这里用不到它们,因此您可以将其省略。
val cupcake: (Int) -> String = {
    "Have a cupcake!"
}
  1. 更新对 trickOrTreat() 函数的调用。对于第一次调用,当 isTrickfalse 时,传入 coins() 函数。对于第二次调用,当 isTricktrue 时,传入 cupcake() 函数。
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val cupcake: (Int) -> String = {
        "Have a cupcake!"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, cupcake)
    treatFunction()
    trickFunction()
}
  1. 运行您的代码。仅当 isTrick 参数设置为 false 实参时,系统才会调用 extraTreat() 函数,因此输出会包含 5 个 25 美分硬币,但不包含纸杯蛋糕。
5 quarters
Have a treat!
No treats!

可为 null 的函数类型

与其他数据类型一样,函数类型可声明为可为 null。在这些情况下,变量可以包含函数,也可以为 null

如需将函数声明为可为 null,请用圆括号括住函数类型,并在右圆括号外后接 ? 符号。例如,如果您想让 () -> String 类型可为 null,则将其声明为 (() -> String)? 类型。语法如下图所示:

c8a004fbdc7469d.png

extraTreat 参数设置为可为 null,这样您就不必在每次调用 trickOrTreat() 函数时都提供 extraTreat() 函数:

  1. extraTreat 参数的类型更改为 (() -> String)?
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
  1. 将对 extraTreat() 函数的调用修改为使用 if 语句,以便仅在该函数为非 null 时才调用该函数。现在,trickOrTreat() 函数应如以下代码段所示:
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        if (extraTreat != null) {
            println(extraTreat(5))
        }
        return treat
    }
}
  1. 移除 cupcake() 函数,然后在对 trickOrTreat() 的第二次调用中将 cupcake 参数替换为 null
fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. 运行您的代码。输出应保持不变。现在,您可以将函数类型声明为可为 null,无需再为 extraTreat 参数传入函数。
5 quarters
Have a treat!
No treats!

5. 使用简写语法编写 lambda 表达式

lambda 表达式提供了多种方式来让您的代码更简洁。在本节中,您将探索其中的一些方式,因为您遇到和编写的大多数 lambda 表达式都是使用简写语法编写的。

省略参数名称

在编写 coins() 函数时,您为函数的 Int 参数明确声明了名称 quantity。不过,与使用 cupcake() 函数时一样,您可以完全省略参数名称。如果函数只有一个参数,而您未提供名称,Kotlin 会隐式为其分配 it 名称,因此您可以省略参数名称和 -> 符号,从而使 lambda 表达式变得更简洁。语法如下图所示:

332ea7bade5062d6.png

更新 coins() 函数以使用参数的简写语法:

  1. coins() 函数中,移除 quantity 参数名称和 -> 符号。
val coins: (Int) -> String = {
    "$quantity quarters"
}
  1. 使用 $it"$quantity quarters" 字符串模板更改为引用单个参数。
val coins: (Int) -> String = {
    "$it quarters"
}
  1. 运行您的代码。Kotlin 可识别 Int 参数的 it 参数名称,并仍会输出 25 美分硬币的数量。
5 quarters
Have a treat!
No treats!

将 lambda 表达式直接传入函数

目前仅在一个位置使用了 coins() 函数。如果您只需将 lambda 表达式直接传入 trickOrTreat() 函数,而无需先创建变量呢?

lambda 表达式只是函数字面量,就像 0 是整数字面量或 "Hello" 是字符串字面量一样。您可以将 lambda 表达式直接传入函数调用。语法如下图所示:

39dc1086e2471ffc.png

修改代码,以便您可以移除 coins 变量:

  1. 移动 lambda 表达式,使其直接传入对 trickOrTreat() 函数的调用。您还可以将 lambda 表达式压缩为一行代码。
fun main() {
    val coins: (Int) -> String = {
        "$it quarters"
    }
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. 移除 coins 变量,因为已经用不到它了。
fun main() {
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}
  1. 运行代码。代码仍会按预期编译和运行。
5 quarters
Have a treat!
No treats!

使用尾随 lambda 语法

当函数类型是函数的最后一个参数时,您可以使用另一个简写选项来编写 lambda。在这种情况下,您可以将 lambda 表达式放在右圆括号后面以调用函数。语法如下图所示:

3ee3176d612b54.png

这会使代码的可读性更强,因为它将 lambda 表达式与其他参数隔开,但没有改变代码的作用。

将代码更改为使用尾随 lambda 语法:

  1. treatFunction 变量中,将 lambda 表达式 {"$it quarters"} 移到对 trickOrTreat() 的调用中的右圆括号后面。
val treatFunction = trickOrTreat(false) { "$it quarters" }
  1. 运行您的代码。仍然一切正常!
5 quarters
Have a treat!
No treats!

6. 使用 repeat() 函数

如果一个函数会返回或接受另一个函数作为实参,该函数就称为高阶函数。trickOrTreat() 函数是一个高阶函数示例,因为它接受 ((Int) -> String)? 类型的函数作为参数,并返回 () -> Unit 类型的函数。Kotlin 提供了几个有用的高阶函数,您可以利用新掌握的 lambda 知识对这些函数加以利用。

repeat() 函数就是这样一种高阶函数。repeat() 函数是使用函数表达 for 循环的简洁方式。在后续单元中,您会经常使用该函数以及其他高阶函数。repeat() 函数具有以下函数签名:

repeat(times: Int, action: (Int) -> Unit)

times 参数是操作应发生的次数。action 参数是一个函数,它接受单个 Int 参数并返回 Unit 类型的函数。action 函数的 Int 参数是到目前为止已执行的操作次数,例如第一次迭代的 0 实参或第二次迭代的 1 实参。您可以使用 repeat() 函数按指定次数重复执行代码,这与 for 循环类似。语法如下图所示:

519a2e0f5d02687.png

您可以使用 repeat() 函数多次调用 trickFunction() 函数,而不是只调用一次。

更新 trick-or-treating 代码以查看 repeat() 函数的实际运用:

  1. main() 函数中,在对 treatFunction()trickFunction() 的调用之间调用 repeat() 函数。为 times 参数传入 4,并为 action 函数使用尾随 lambda 语法。您无需为 lambda 表达式的 Int 参数提供名称。
fun main() {
    val treatFunction = trickOrTreat(false) { "$it quarters" }
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
    repeat(4) {

    }
}
  1. 将对 treatFunction() 函数的调用移到 repeat() 函数的 lambda 表达式中。
fun main() {
    val treatFunction = trickOrTreat(false) { "$it quarters" }
    val trickFunction = trickOrTreat(true, null)
    repeat(4) {
        treatFunction()
    }
    trickFunction()
}
  1. 运行您的代码。"Have a treat" 字符串应输出 4 次。
5 quarters
Have a treat!
Have a treat!
Have a treat!
Have a treat!
No treats!

7. 总结

恭喜!您已经学习了函数类型和 lambda 表达式的基础知识。熟悉这些概念有助于您深入学习 Kotlin 语言。对函数类型、高阶函数和简写语法的运用还能让您的代码变得更简洁、更易读。

总结

  • Kotlin 中的函数是一级结构,可以视为数据类型。
  • lambda 表达式提供了一种用于编写函数的简写语法。
  • 您可以将函数类型传入其他函数。
  • 对于一个函数类型,您可以从另一个函数返回它。
  • lambda 表达式会返回最后一个表达式的值。
  • 如果只有一个参数的 lambda 表达式中省略了某个参数标签,系统会使用 it 标识符来引用它。
  • lambda 可以内嵌方式编写,无需使用变量名称。
  • 在您调用某个函数时,如果该函数的最后一个参数是函数类型,您可以使用尾随 lambda 语法将 lambda 表达式移至最后一个圆括号后面。
  • 高阶函数是指接受函数作为参数或返回函数的函数。
  • repeat() 函数是一个高阶函数,其工作方式与 for 循环类似。

了解详情