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

1. 简介

本 Codelab 将教您函数类型、如何使用函数类型以及 Lambda 表达式特有的语法。

在 Kotlin 中,函数被视为一等构造(first-class constructs)。这意味着函数可以像数据类型一样对待。您可以将函数存储在变量中,将它们作为参数传递给其他函数,以及从其他函数返回函数。

与其他可以使用字面值表示的数据类型一样——例如值 10Int 类型,以及值 "Hello"String 类型——您也可以声明函数字面值,它们称为 Lambda 表达式或简称为 Lambda。在 Android 开发中以及更广泛的 Kotlin 编程中,您会大量使用 Lambda 表达式。

前提条件

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

您将学到什么

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

您需要准备什么

  • 可访问 Kotlin Playground 的网页浏览器

2. 观看同步编码视频(可选)

如果您想观看其中一位课程讲师完成此 Codelab,请播放下面的视频。

建议将视频展开至全屏(使用视频角落的此图标 This symbol shows 4 corners on a square highlighted, to indicate full screen mode.),以便您更清楚地看到 Kotlin Playground 和代码。

此步骤是可选的。您也可以跳过视频,直接开始 Codelab 说明。

3. 将函数存储在变量中

到目前为止,您学习了如何使用 fun 关键字声明函数。使用 fun 关键字声明的函数可以调用,从而导致函数体中的代码执行。

作为一等构造,函数也是数据类型,因此您可以将函数存储在变量中,将它们传递给函数,并从函数返回它们。也许您希望能够在运行时更改应用某个部分的 behavior,或者像之前 Codelab 中那样嵌套 composable 函数来构建布局。所有这一切都可通过 Lambda 表达式实现。

您可以通过一些不给糖就捣蛋(Trick-or-treating)来了解其实际应用,这指的是许多国家/地区的万圣节传统,孩子们穿着盛装挨家挨户地问“不给糖就捣蛋”,通常会收到糖果作为回报。

将函数存储在变量中

  1. 前往Kotlin Playground
  2. main() 函数之后,定义一个不带参数且没有返回值的 trick() 函数,该函数打印 "No treats!"。其语法与您在之前 Codelabs 中看到的其他函数相同。
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 的变量,使其等于一个 Lambda 表达式,其函数体打印 "Have a treat!"
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() 函数之后,定义一个 trickOrTreat() 函数,该函数接受类型为 BooleanisTrick 参数。
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. 运行代码。extraTreat() 函数仅在 isTrick 参数设置为 false 参数时被调用,因此输出包括 5 个 quarters,但没有 cupcakes。
5 quarters
Have a treat!
No treats!

可空函数类型

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

要将函数声明为可空,请将函数类型用圆括号括起来,并在结束圆括号后添加 ? 符号。例如,如果您想使 () -> String 类型可空,请将其声明为 (() -> String)? 类型。语法如这张图所示:

c8a004fbdc7469d.png

extraTreat 参数设为可空,这样您就不必每次调用 trickOrTreat() 函数时都提供一个 extraTreat() 函数。

  1. extraTreat 参数的类型更改为 (() -> String)?
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
  1. 修改对 extraTreat() 函数的调用,使用 if 语句仅在函数非空时调用。现在 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. 运行代码。输出应该保持不变。现在您可以将函数类型声明为可空,无需再为 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. "$quantity quarters" 字符串模板更改为使用 $it 来引用单个参数。
val coins: (Int) -> String = {
    "$it quarters"
}
  1. 运行代码。Kotlin 识别 Int 参数的 it 参数名,并仍然打印 quarters 的数量。
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() 函数,而不是只调用一次。

更新您的不给糖就捣蛋代码以查看 repeat() 函数的实际应用

  1. main() 函数中,在调用 treatFunction()trickFunction() 之间,调用 repeat() 函数。将 4 传入 times 参数,并对 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" 字符串应该打印四次。
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 循环。

了解更多