1. 简介
本 Codelab 教您了解函数类型、如何使用函数类型以及 Lambda 表达式特有的语法。
在 Kotlin 中,函数被认为是一等公民。这意味着函数可以像数据类型一样对待。您可以将函数存储在变量中,将它们作为参数传递给其他函数,并从其他函数中返回它们。
与其他您可以用字面量表示的数据类型(如值为 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
关键字声明的函数可以被调用,这将导致函数体中的代码执行。
作为一等公民,函数也是数据类型,因此您可以将函数存储在变量中,将它们传递给函数,并从函数中返回它们。也许您希望能够在运行时更改应用程序一部分的行为,或者嵌套可组合函数以构建布局,就像您在之前的 Codelab 中所做的那样。Lambda 表达式使这一切成为可能。
您可以在一些 不给糖就捣乱 中看到这一点,它指的是许多国家在万圣节期间的一种传统,孩子们穿着戏服挨家挨户地走访,并索要“不给糖就捣乱”,通常是为了换取糖果。
将函数存储在变量中
- 导航到 Kotlin Playground。
- 在
main()
函数之后,定义一个名为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
的变量,该变量等于一个 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()
函数之后,定义一个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()
的结果,将false
传递给isTrick
参数。然后,创建第二个变量,名为trickFunction
,并将其分配给调用trickOrTreat()
的结果,这次将true
传递给isTrick
参数。
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 个 25 美分硬币,但没有纸杯蛋糕。
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
参数名称,并且仍然打印出 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
循环。语法在此图片中说明
与其只调用一次 trickFunction()
函数,您可以使用 repeat()
函数多次调用它。
更新您的 trick-or-treating 代码,以查看 repeat()
函数的实际应用。
- 在
main()
函数中,在调用treatFunction()
和trickFunction()
之间调用repeat()
函数。将4
传递给times
参数,并使用尾随 lambda 语法来使用action
函数。您不需要为 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
循环。