1. 简介
在此 Codelab 中,您将了解函数类型、函数类型的用法以及特定于 lambda 表达式的语法。
在 Kotlin 中,函数被视为一级结构。这意味着函数可以被视为数据类型。您可以将函数存储在变量中,将函数作为参数传递到其他函数,以及从其他函数返回函数。
与您可以使用字面量值表示的其他数据类型(例如值为 10
的 Int
类型和值为 "Hello"
的 String
类型)一样,您也可以声明函数字面量(称为 lambda 表达式,也可简称为 lambda)。在 Android 开发中,您会广泛使用 lambda 表达式;在 Kotlin 编程中,lambda 表达式的使用就更广泛了。
前提条件
- 熟悉 Kotlin 编程,包括函数、
if/else
语句和可为 null 性
学习内容
- 如何使用 lambda 语法定义函数。
- 如何将函数存储在变量中。
- 如何将函数作为参数传递到其他函数。
- 如何从其他函数返回函数。
- 如何使用可为 null 的函数类型。
- 如何使 lambda 表达式更简洁。
- 什么是高阶函数。
- 如何使用
repeat()
函数。
所需条件
- 一个能够访问 Kotlin 园地的网络浏览器
2. 观看配套代码演示视频(可选)
如果您想要观看某位课程讲师完成此 Codelab 的过程,请播放以下视频。
建议将视频全屏展开(使用视频一角的 图标),以便更清楚地查看 Kotlin 园地和相关代码。
这是可选步骤。您也可以跳过视频,立即开始按照此 Codelab 中的说明操作。
3. 将函数存储在变量中
到目前为止,您已经学习了如何使用 fun
关键字来声明函数。使用 fun
关键字声明的函数可供调用,调用后,函数正文中的代码就会执行。
作为一种一级结构,函数也属于数据类型,因此,您可以将函数存储在变量中、将函数传递到函数,以及从函数返回函数。或许,您希望在运行时更改应用中某一部分的行为,或嵌入可组合函数以构建布局,就像您在之前的 Codelab 中所做的那样。这一切都可通过 lambda 表达式来实现。
您可以参照一些 trick-or-treating(不给糖就捣蛋)代码来理解该过程的实际运作。“不给糖就捣蛋”是指许多国家/地区过万圣节时的传统习俗:万圣节期间,孩子们会穿着万圣节的服饰挨家挨户地说“不给糖就捣蛋”,他们通常能换到糖果。
将函数存储在变量中:
- 访问 Kotlin 园地。
- 在
main()
函数后面,定义一个不带参数和返回值且输出"No treats!"
的trick()
函数。其语法与您在之前的 Codelab 中看到的其他函数的语法相同。
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
的变量,使其与正文输出"Have a treat!"
的 lambda 表达式相等。
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()
函数后面,定义一个接受Boolean
类型的isTrick
参数的trickOrTreat()
函数。
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()
}
- 运行您的代码。仅当
isTrick
参数设置为false
实参时,系统才会调用extraTreat()
函数,因此输出会包含 5 个 25 美分硬币,但不包含纸杯蛋糕。
5 quarters Have a treat! No treats!
可为 null 的函数类型
与其他数据类型一样,函数类型可声明为可为 null。在这些情况下,变量可以包含函数,也可以为 null
。
如需将函数声明为可为 null,请用圆括号括住函数类型,并在右圆括号外后接 ?
符号。例如,如果您想让 () -> String
类型可为 null,则将其声明为 (() -> String)?
类型。语法如下图所示:
将 extraTreat
参数设置为可为 null,这样您就不必在每次调用 trickOrTreat()
函数时都提供 extraTreat()
函数:
- 将
extraTreat
参数的类型更改为(() -> String)?
。
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
- 将对
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
}
}
- 移除
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()
}
- 运行您的代码。输出应保持不变。现在,您可以将函数类型声明为可为 null,无需再为
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"
}
- 使用
$it
将"$quantity quarters"
字符串模板更改为引用单个参数。
val coins: (Int) -> String = {
"$it quarters"
}
- 运行您的代码。Kotlin 可识别
Int
参数的it
参数名称,并仍会输出 25 美分硬币的数量。
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()
函数,而不是只调用一次。
更新 trick-or-treating 代码以查看 repeat()
函数的实际运用:
- 在
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) {
}
}
- 将对
treatFunction()
函数的调用移到repeat()
函数的 lambda 表达式中。
fun main() {
val treatFunction = trickOrTreat(false) { "$it quarters" }
val trickFunction = trickOrTreat(true, null)
repeat(4) {
treatFunction()
}
trickFunction()
}
- 运行您的代码。
"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
循环类似。