1. 简介
本 Codelab 将教您函数类型、如何使用函数类型以及 Lambda 表达式特有的语法。
在 Kotlin 中,函数被视为一等构造(first-class constructs)。这意味着函数可以像数据类型一样对待。您可以将函数存储在变量中,将它们作为参数传递给其他函数,以及从其他函数返回函数。
与其他可以使用字面值表示的数据类型一样——例如值 10
的 Int
类型,以及值 "Hello"
的 String
类型——您也可以声明函数字面值,它们称为 Lambda 表达式或简称为 Lambda。在 Android 开发中以及更广泛的 Kotlin 编程中,您会大量使用 Lambda 表达式。
前提条件
- 熟悉 Kotlin 编程,包括函数、
if/else
语句和可空性
您将学到什么
- 如何使用 Lambda 语法定义函数。
- 如何将函数存储在变量中。
- 如何将函数作为参数传递给其他函数。
- 如何从其他函数返回函数。
- 如何使用可空函数类型。
- 如何使 Lambda 表达式更简洁。
- 什么是高阶函数。
- 如何使用
repeat()
函数。
您需要准备什么
- 可访问 Kotlin Playground 的网页浏览器
2. 观看同步编码视频(可选)
如果您想观看其中一位课程讲师完成此 Codelab,请播放下面的视频。
建议将视频展开至全屏(使用视频角落的此图标 ),以便您更清楚地看到 Kotlin Playground 和代码。
此步骤是可选的。您也可以跳过视频,直接开始 Codelab 说明。
3. 将函数存储在变量中
到目前为止,您学习了如何使用 fun
关键字声明函数。使用 fun
关键字声明的函数可以调用,从而导致函数体中的代码执行。
作为一等构造,函数也是数据类型,因此您可以将函数存储在变量中,将它们传递给函数,并从函数返回它们。也许您希望能够在运行时更改应用某个部分的 behavior,或者像之前 Codelab 中那样嵌套 composable 函数来构建布局。所有这一切都可通过 Lambda 表达式实现。
您可以通过一些不给糖就捣蛋(Trick-or-treating)来了解其实际应用,这指的是许多国家/地区的万圣节传统,孩子们穿着盛装挨家挨户地问“不给糖就捣蛋”,通常会收到糖果作为回报。
将函数存储在变量中
- 前往Kotlin Playground。
- 在
main()
函数之后,定义一个不带参数且没有返回值的trick()
函数,该函数打印"No treats!"
。其语法与您在之前 Codelabs 中看到的其他函数相同。
fun main() {
}
fun trick() {
println("No treats!")
}
- 在
main()
函数体中,创建一个名为trickFunction
的变量,并将其设置为等于trick
。您无需在trick
后添加括号,因为您希望将函数存储在变量中,而不是调用该函数。
fun main() {
val trickFunction = trick
}
fun trick() {
println("No treats!")
}
- 运行代码。会产生错误,因为 Kotlin 编译器将
trick
识别为trick()
函数的名称,但它期望您调用该函数,而不是将其赋值给变量。
Function invocation 'trick()' expected
您尝试将 trick
存储在 trickFunction
变量中。但是,要将函数作为值引用,您需要使用函数引用运算符(::
)。语法如这张图所示:
- 要将函数作为值引用,请将
trickFunction
重新赋值为::trick
。
fun main() {
val trickFunction = ::trick
}
fun trick() {
println("No treats!")
}
- 运行代码以验证是否没有更多错误。您会看到一条警告,提示
trickFunction
从未使用过,但这将在下一部分中修复。
使用 Lambda 表达式重新定义函数
Lambda 表达式提供了一种简洁的语法,无需 fun
关键字即可定义函数。您可以将 Lambda 表达式直接存储在变量中,而无需对另一个函数进行函数引用。
在赋值运算符(=
)之前,您添加 val
或 var
关键字,后跟变量名,这是您调用函数时使用的名称。在赋值运算符(=
)之后是 Lambda 表达式,它由一对花括号组成,形成函数体。语法如这张图所示:
使用 Lambda 表达式定义函数时,您会得到一个引用该函数的变量。您也可以像任何其他类型一样将其值赋给其他变量,并使用新变量的名称调用该函数。
更新代码以使用 Lambda 表达式
- 使用 Lambda 表达式重写
trick()
函数。trick
名称现在指向变量名。花括号中的函数体现在是一个 Lambda 表达式。
fun main() {
val trickFunction = ::trick
}
val trick = {
println("No treats!")
}
- 在
main()
函数中,移除函数引用运算符(::
),因为trick
现在引用的是一个变量,而不是函数名。
fun main() {
val trickFunction = trick
}
val trick = {
println("No treats!")
}
- 运行代码。没有错误,您可以直接引用
trick()
函数,而无需函数引用运算符(::
)。没有输出,因为您还没有调用该函数。 - 在
main()
函数中,调用trick()
函数,但这次要像调用任何其他函数一样包含括号。
fun main() {
val trickFunction = trick
trick()
}
val trick = {
println("No treats!")
}
- 运行代码。Lambda 表达式的主体被执行。
No treats!
- 在
main()
函数中,像调用函数一样调用trickFunction
变量。
fun main() {
val trickFunction = trick
trick()
trickFunction()
}
val trick = {
println("No treats!")
}
- 运行代码。函数被调用了两次,一次是
trick()
函数调用,第二次是trickFunction()
函数调用。
No treats! No treats!
使用 Lambda 表达式,您可以创建存储函数的变量,像调用函数一样调用这些变量,并将它们存储在您可以像调用函数一样调用的其他变量中。
4. 将函数用作数据类型
您在之前的 Codelab 中了解到,Kotlin 具有类型推断。声明变量时,通常不需要显式指定类型。在前面的示例中,Kotlin 编译器能够推断出 trick
的值是一个函数。但是,如果您想指定函数参数或返回值的类型,则需要了解表示函数类型的语法。函数类型由一对圆括号组成,其中包含可选的参数列表、->
符号和返回类型。语法如这张图所示:
您之前声明的 trick
变量的数据类型是 () -> Unit
。圆括号为空,因为函数没有参数。返回类型是 Unit
,因为函数不返回任何内容。如果您有一个接受两个 Int
参数并返回一个 Int
的函数,则其数据类型将是 (Int, Int) -> Int
。
使用 Lambda 表达式声明另一个显式指定函数类型的函数
- 在
trick
变量之后,声明一个名为treat
的变量,使其等于一个 Lambda 表达式,其函数体打印"Have a treat!"
。
val trick = {
println("No treats!")
}
val treat = {
println("Have a treat!")
}
- 将
treat
变量的数据类型指定为() -> Unit
。
val treat: () -> Unit = {
println("Have a treat!")
}
- 在
main()
函数中,调用treat()
函数。
fun main() {
val trickFunction = trick
trick()
trickFunction()
treat()
}
- 运行代码。
treat()
函数的行为与trick()
函数类似。尽管只有treat
变量显式声明了数据类型,但这两个变量都具有相同的数据类型。
No treats! No treats! Have a treat!
将函数用作返回类型
函数是一种数据类型,因此您可以像使用其他任何数据类型一样使用它。您甚至可以从其他函数返回函数。语法如这张图所示:
创建一个返回函数的函数。
- 删除
main()
函数中的代码。
fun main() {
}
- 在
main()
函数之后,定义一个trickOrTreat()
函数,该函数接受类型为Boolean
的isTrick
参数。
fun main() {
}
fun trickOrTreat(isTrick: Boolean): () -> Unit {
}
val trick = {
println("No treats!")
}
val treat = {
println("Have a treat!")
}
- 在
trickOrTreat()
函数体中,添加一个if
语句,如果isTrick
为true
,则返回trick()
函数;如果isTrick
为 false,则返回treat()
函数。
fun trickOrTreat(isTrick: Boolean): () -> Unit {
if (isTrick) {
return trick
} else {
return treat
}
}
- 在
main()
函数中,创建一个名为treatFunction
的变量,并将其赋值为调用trickOrTreat()
的结果,其中isTrick
参数传入false
。然后,创建第二个变量,名为trickFunction
,并将其赋值为调用trickOrTreat()
的结果,这次isTrick
参数传入true
。
fun main() {
val treatFunction = trickOrTreat(false)
val trickFunction = trickOrTreat(true)
}
- 调用
treatFunction()
,然后在下一行调用trickFunction()
。
fun main() {
val treatFunction = trickOrTreat(false)
val trickFunction = trickOrTreat(true)
treatFunction()
trickFunction()
}
- 运行代码。您应该会看到每个函数的输出。即使您没有直接调用
trick()
或treat()
函数,您仍然可以调用它们,因为您存储了每次调用trickOrTreat()
函数的返回值,并使用trickFunction
和treatFunction
变量调用了这些函数。
Have a treat! No treats!
现在您了解了函数如何返回其他函数。您还可以将函数作为参数传递给另一个函数。也许您想为 trickOrTreat()
函数提供一些自定义行为,使其执行除返回两个字符串之一以外的其他操作。接受另一个函数作为参数的函数允许您每次调用时传递不同的函数。
将函数作为参数传递给另一个函数
在世界上一些庆祝万圣节的地方,孩子们会收到零钱而不是糖果,或者两者都收到。您将修改 trickOrTreat()
函数,允许将由函数表示的额外款待作为参数提供。
作为 trickOrTreat()
参数使用的函数也需要有自己的参数。声明函数类型时,参数没有标签。您只需要指定每个参数的数据类型,并用逗号分隔。语法如这张图所示:
当您为接受参数的函数编写 Lambda 表达式时,参数会按照出现的顺序获得名称。参数名称列在开花括号之后,每个名称用逗号分隔。箭头(->
)将参数名称与函数体分隔开。语法如这张图所示:
更新 trickOrTreat()
函数以接受函数作为参数
- 在
isTrick
参数之后,添加一个类型为(Int) -> String
的extraTreat
参数。
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
- 在
else
块中,在return
语句之前,调用println()
,并在其中传入对extraTreat()
函数的调用。将5
传入对extraTreat()
的调用。
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
if (isTrick) {
return trick
} else {
println(extraTreat(5))
return treat
}
}
- 现在,当您调用
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()
}
- 在
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()
}
- 在
cupcake()
函数中,移除quantity
参数和->
符号。它们没有被使用,所以可以省略。
val cupcake: (Int) -> String = {
"Have a cupcake!"
}
- 更新对
trickOrTreat()
函数的调用。对于第一次调用,当isTrick
为false
时,传入coins()
函数。对于第二次调用,当isTrick
为true
时,传入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()
}
- 运行代码。
extraTreat()
函数仅在isTrick
参数设置为false
参数时被调用,因此输出包括 5 个 quarters,但没有 cupcakes。
5 quarters Have a treat! No treats!
可空函数类型
与其他数据类型一样,函数类型可以声明为可空。在这种情况下,变量可以包含一个函数,也可以是 null
。
要将函数声明为可空,请将函数类型用圆括号括起来,并在结束圆括号后添加 ?
符号。例如,如果您想使 () -> String
类型可空,请将其声明为 (() -> String)?
类型。语法如这张图所示:
将 extraTreat
参数设为可空,这样您就不必每次调用 trickOrTreat()
函数时都提供一个 extraTreat()
函数。
- 将
extraTreat
参数的类型更改为(() -> String)?
。
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
- 修改对
extraTreat()
函数的调用,使用if
语句仅在函数非空时调用。现在trickOrTreat()
函数应如下所示:
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
if (isTrick) {
return trick
} else {
if (extraTreat != null) {
println(extraTreat(5))
}
return treat
}
}
- 移除
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()
}
- 运行代码。输出应该保持不变。现在您可以将函数类型声明为可空,无需再为
extraTreat
参数传入函数。
5 quarters Have a treat! No treats!
5. 使用速记语法编写 Lambda 表达式
Lambda 表达式提供了多种使代码更简洁的方法。您将在本节中探索其中几种,因为您遇到和编写的大多数 Lambda 表达式都使用速记语法编写。
省略参数名
编写 coins()
函数时,您为函数的 Int
参数显式声明了名称 quantity
。然而,正如您在 cupcake()
函数中看到的那样,您可以完全省略参数名。当函数只有一个参数且您未提供名称时,Kotlin 会隐式将其命名为 it
,因此您可以省略参数名和 ->
符号,这使得您的 Lambda 表达式更加简洁。语法如这张图所示:
更新 coins()
函数以使用参数的速记语法
- 在
coins()
函数中,移除quantity
参数名和->
符号。
val coins: (Int) -> String = {
"$quantity quarters"
}
- 将
"$quantity quarters"
字符串模板更改为使用$it
来引用单个参数。
val coins: (Int) -> String = {
"$it quarters"
}
- 运行代码。Kotlin 识别
Int
参数的it
参数名,并仍然打印 quarters 的数量。
5 quarters Have a treat! No treats!
将 Lambda 表达式直接传入函数
coins()
函数目前只在一个地方使用。如果您可以直接将 Lambda 表达式传入 trickOrTreat()
函数,而无需先创建一个变量呢?
Lambda 表达式仅仅是函数字面值,就像 0
是整数字面值或 "Hello"
是字符串字面值一样。您可以将 Lambda 表达式直接传入函数调用。语法如这张图所示:
修改代码以移除 coins
变量
- 移动 Lambda 表达式,使其直接传入对
trickOrTreat()
函数的调用。您还可以将 Lambda 表达式精简到一行。
fun main() {
val coins: (Int) -> String = {
"$it quarters"
}
val treatFunction = trickOrTreat(false, { "$it quarters" })
val trickFunction = trickOrTreat(true, null)
treatFunction()
trickFunction()
}
- 移除
coins
变量,因为它不再使用。
fun main() {
val treatFunction = trickOrTreat(false, { "$it quarters" })
val trickFunction = trickOrTreat(true, null)
treatFunction()
trickFunction()
}
- 运行代码。它仍然可以编译并按预期运行。
5 quarters Have a treat! No treats!
使用尾随 Lambda 语法
当函数类型是函数的最后一个参数时,您可以使用另一种速记选项来编写 Lambda。在这种情况下,您可以将 Lambda 表达式放在调用函数的闭括号之后。语法如这张图所示:
这使得您的代码更具可读性,因为它将 Lambda 表达式与其他参数分开,但不会改变代码的功能。
更新代码以使用尾随 Lambda 语法
- 在
treatFunction
变量中,将 Lambda 表达式{"$it quarters"}
移动到对trickOrTreat()
的调用的闭括号之后。
val treatFunction = trickOrTreat(false) { "$it quarters" }
- 运行代码。一切仍然正常工作!
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
循环。语法如这张图所示:
您可以使用 repeat()
函数多次调用 trickFunction()
函数,而不是只调用一次。
更新您的不给糖就捣蛋代码以查看 repeat()
函数的实际应用
- 在
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) {
}
}
- 将对
treatFunction()
函数的调用移动到repeat()
函数的 Lambda 表达式中。
fun main() {
val treatFunction = trickOrTreat(false) { "$it quarters" }
val trickFunction = trickOrTreat(true, null)
repeat(4) {
treatFunction()
}
trickFunction()
}
- 运行代码。
"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
循环。