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

1. 简介

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

在 Kotlin 中,函数被视为一等构造。这意味着函数可以被视为一种数据类型。您可以将函数存储在变量中,将其作为参数传递给其他函数,并从其他函数返回它们。

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

先决条件

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

您将学到什么

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

您需要什么

  • 可以访问 Kotlin Playground 的 Web 浏览器

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

如果您想观看课程讲师完成 Codelab 的过程,请播放下面的视频。

建议将视频扩展到全屏(使用此图标 此符号显示突出显示的正方形上的 4 个角,表示全屏模式。 视频角落),以便您可以更清晰地看到 Kotlin Playground 和代码。

此步骤是可选的。您也可以跳过视频并立即开始 Codelab 说明。

3. 将函数存储在变量中

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

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

您可以通过一些 不给糖就捣蛋 来看到这一点,这指的是许多国家在万圣节期间的传统,孩子们穿着服装挨家挨户地索要糖果。

将函数存储在变量中

  1. 导航到 Kotlin Playground
  2. main() 函数之后,定义一个没有参数且没有返回值的 trick() 函数,该函数打印 "No treats!"。语法与您在之前的 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 的变量,将其设置为一个 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 个 25 美分硬币,但不包括纸杯蛋糕。
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参数名称,并仍然打印四分之一的数量。
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()函数。为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"字符串应打印四次。
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 循环。

了解更多