此 Codelab 是面向编程人员的 Kotlin 训练营课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。根据您的知识水平,对于某些版块您也许只需要略读即可。本课程专为了解面向对象的语言并想学习 Kotlin 的编程人员而设计。
简介
在此 Codelab 中,您将创建一个 Kotlin 程序并了解 Kotlin 中的函数,包括参数、过滤条件、lambda 和 compact 函数的默认值。
本课程中的各个课程旨在帮助您积累知识,但彼此之间半独立,因此您可以略读自己熟悉的部分,而不是构建单个示例应用。为了将这些示例联系起来,我们使用了水族馆主题。如果您想了解完整的水族馆故事,请查看 Kotlin 编程人员训练营 Udacity 课程。
您应当已掌握的内容
- 现代的面向对象静态类型编程语言的基础知识
- 如何使用类、方法和异常处理以至少一种语言进行编程
- 如何在 IntelliJ IDEA 中使用 Kotlin 的 REPL(读取-求值-输出循环)
- Kotlin 的基础知识,包括类型、运算符和循环
本 Codelab 专为了解面向对象的语言并想详细了解 Kotlin 的程序员设计。
学习内容
- 如何在 IntelliJ IDEA 中使用
main()函数和实参创建程序 - 如何使用默认值和紧凑型函数
- 如何为列表应用过滤条件
- 如何创建基本的 lambda 和高阶函数
您将执行的操作
- 使用 REPL 试用一些代码。
- 使用 IntelliJ IDEA 创建基本的 Kotlin 程序。
在此任务中,您将创建一个 Kotlin 程序,了解 main() 函数,以及如何从命令行向程序传递实参。
您可能还记得在上一个 Codelab 中输入到 REPL 中的 printHello() 函数:
fun printHello() {
println ("Hello World")
}
printHello()⇒ Hello World
如需定义函数,请使用 fun 关键字,后跟函数名称。与其他编程语言一样,圆括号 () 用于函数参数(如有)。大括号 {} 用于括住函数的代码。此函数不存在任何返回值类型,因为此函数不会返回任何内容。
第 1 步:创建 Kotlin 文件
- 打开 IntelliJ IDEA。
- IntelliJ IDEA 左侧的 Project 窗格会显示项目文件和文件夹列表。查找并右键点击 Hello Kotlin 下的 src 文件夹。(您应当已从上一个 Codelab 获得 Hello Kotlin 项目。)
- 依次选择 New > Kotlin File / Class。
- 将 Kind 保留为 File,并将此文件命名为 Hello。
- 点击确定。
现在,src 文件夹中有一个名为 Hello.kt 的文件。

第 2 步:添加代码并运行程序
- 与其他语言一样,Kotlin
main()函数指定了执行的入口点。任何命令行实参都作为字符串数组传递。
将以下代码输入或粘贴到 Hello.kt 文件中:
fun main(args: Array<String>) {
println("Hello, world!")
}与之前的 printHello() 函数一样,此函数没有 return 语句。Kotlin 中的每个函数都会返回一些内容,即使没有明确指定也是如此。因此,像 main() 函数这样的函数会返回 kotlin.Unit 类型,这是 Kotlin 表示没有值的方式。
- 如需运行程序,请点击
main()函数左侧的绿色三角形。从菜单中选择 Run 'HelloKt'。 - IntelliJ IDEA 会编译并运行该程序。结果会显示在底部的日志窗格中,如下所示。

第 3 步:向 main() 传递参数
由于您从 IntelliJ IDEA 而非命令行来运行程序,因此需要为该程序指定任何略有不同的参数。
- 依次选择 Run > Edit Configurations。系统会显示 Run/Debug Configurations 窗口。
- 在 Program arguments 字段中输入
Kotlin!。 - 点击确定。

第 4 步:更改代码以使用字符串模板
字符串模板会将变量或表达式插入字符串中,$ 会将字符串的一部分指定为变量或表达式。大括号 {} 用于括住表达式(如有)。
- 在 Hello.kt 中,更改问候语以使用传递到程序的第一个参数
args[0],而不是"world"。
fun main(args: Array<String>) {
println("Hello, ${args[0]}")
}- 运行程序,输出结果将包含您指定的参数。
⇒ Hello, Kotlin!
在此任务中,您将了解 Kotlin 中的几乎所有内容都具有值的原因,及其具有重要意义的原因。
一些其他语言有语句,这些语句是没有任何值的代码行。在 Kotlin 中,几乎所有内容都是表达式,并且都具有值(即使该值为 kotlin.Unit)。
- 在 Hello.kt 中,在
main()中编写代码,为名为isUnit的变量赋予println()并输出该值。(println()未返回值,因此将返回kotlin.Unit。)
// Will assign kotlin.Unit
val isUnit = println("This is an expression")
println(isUnit)- 运行程序。第一个
println()会输出字符串"This is an expression"。第二个println()会输出第一个println()语句的值,即kotlin.Unit。
⇒ This is an expression kotlin.Unit
- 声明一个名为
temperature的val,并将其初始化为 10。 - 声明另一个名为
isHot的val,并为isHot赋予if/else语句的返回值,如以下代码所示。由于它是一个表达式,因此您可以立即使用if表达式的值。
val temperature = 10
val isHot = if (temperature > 50) true else false
println(isHot)⇒ false
- 在字符串模板中使用表达式的值。添加一些代码来检查水温,从而确定水温是否适宜鱼类生存或过热,然后运行程序。
val temperature = 10
val message = "The water temperature is ${ if (temperature > 50) "too warm" else "OK" }."
println(message)⇒ The water temperature is OK.
在此任务中,您将详细了解 Kotlin 中的函数,并详细了解非常有用的 when 条件表达式。
第 1 步:创建一些函数
在此步骤中,您将利用学到的一些知识,并创建不同类型的函数。您可以使用以下新代码替换 Hello.kt 的内容。
- 编写一个名为
feedTheFish()的函数,该函数调用randomDay()以获取一周中的随机日期。使用字符串模板,输出鱼类当天吃的food。目前,鱼类每天吃同一种食物。
fun feedTheFish() {
val day = randomDay()
val food = "pellets"
println ("Today is $day and the fish eat $food")
}
fun main(args: Array<String>) {
feedTheFish()
}- 编写
randomDay()函数,以从数组中选择随机日期并返回。
nextInt() 函数采用整数限制,这会将数字从 Random() 限制到 0 至 6,以与 week 数组匹配。
fun randomDay() : String {
val week = arrayOf ("Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday")
return week[Random().nextInt(week.size)]
}Random()和nextInt()函数在java.util.*中定义。在文件顶部添加所需导入:
import java.util.* // required import- 运行程序并检查输出结果。
⇒ Today is Tuesday and the fish eat pellets
第 2 步:使用 when 表达式
进一步扩展,更改代码以使用 when 表达式为不同日期选择不同食物。在其他编程语言中,when 语句类似于 switch,但 when 会在每个分支结束时自动中断。在您检查枚举的情况下,它还可确保您的代码覆盖所有分支。
- 在 Hello.kt 中,添加一个名为
fishFood()的函数,该函数将某个日期作为String,并以String形式返回鱼类当天吃的食物。使用when(),确保鱼类每天获得特定食物。运行程序几次,以查看不同的输出结果。
fun fishFood (day : String) : String {
var food = ""
when (day) {
"Monday" -> food = "flakes"
"Tuesday" -> food = "pellets"
"Wednesday" -> food = "redworms"
"Thursday" -> food = "granules"
"Friday" -> food = "mosquitoes"
"Saturday" -> food = "lettuce"
"Sunday" -> food = "plankton"
}
return food
}
fun feedTheFish() {
val day = randomDay()
val food = fishFood(day)
println ("Today is $day and the fish eat $food")
}⇒ Today is Thursday and the fish eat granules
- 使用
else向when表达式添加默认分支。对于测试,为确保有时在程序中采用默认值,请移除Tuesday和Saturday分支。
使用默认分支可确保food在返回之前获得一个值,因此不再需要对其进行初始化。由于代码现在仅向food分配字符串一次,因此您可以使用val而非var声明food。
fun fishFood (day : String) : String {
val food : String
when (day) {
"Monday" -> food = "flakes"
"Wednesday" -> food = "redworms"
"Thursday" -> food = "granules"
"Friday" -> food = "mosquitoes"
"Sunday" -> food = "plankton"
else -> food = "nothing"
}
return food
}- 由于每个表达式都具有值,因此您可以使此代码更简洁一些。直接返回
when表达式的值,并清除food变量。when表达式的值是满足条件的分支的最后一个表达式的值。
fun fishFood (day : String) : String {
return when (day) {
"Monday" -> "flakes"
"Wednesday" -> "redworms"
"Thursday" -> "granules"
"Friday" -> "mosquitoes"
"Sunday" -> "plankton"
else -> "nothing"
}
}程序的最终版本类似于下面的代码。
import java.util.* // required import
fun randomDay() : String {
val week = arrayOf ("Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday")
return week[Random().nextInt(week.size)]
}
fun fishFood (day : String) : String {
return when (day) {
"Monday" -> "flakes"
"Wednesday" -> "redworms"
"Thursday" -> "granules"
"Friday" -> "mosquitoes"
"Sunday" -> "plankton"
else -> "nothing"
}
}
fun feedTheFish() {
val day = randomDay()
val food = fishFood(day)
println ("Today is $day and the fish eat $food")
}
fun main(args: Array<String>) {
feedTheFish()
}在此任务中,您将了解函数和方法的默认值。此外,您还将了解紧凑型函数,此类函数可以使您的代码更加简洁、可读性更高,并且可以减少测试用代码路径的数量。紧凑型函数也称为单表达式函数。
第 1 步:为参数创建默认值
在 Kotlin 中,您可以按形参名称传递实参。此外,您还可以为参数指定默认值:如果调用方未提供参数,系统会使用默认值。稍后,当您编写方法(成员函数)时,这意味着您可以避免编写同一方法的大量重载版本。
- 在 Hello.kt 中,编写一个
swim()函数,其中包含一个名为speed的String参数,用于输出鱼类的速度。speed参数的默认值为"fast"。
fun swim(speed: String = "fast") {
println("swimming $speed")
}- 从
main()函数中,以三种方式调用swim()函数。首先,使用默认值调用该函数。然后,调用该函数并传递未命名的speed参数,然后命名speed参数以调用该函数。
swim() // uses default speed
swim("slow") // positional argument
swim(speed="turtle-like") // named parameter⇒ swimming fast swimming slow swimming turtle-like
第 2 步:添加必需参数
如果未为参数指定默认值,必须始终传递相应的参数。
- 在 Hello.kt 中,编写一个
shouldChangeWater()函数,该函数采用三个参数:day、temperature和dirty级别。如果应当更换水,该函数会返回true,在星期天、温度过高或水过脏时,就会发生这种情况。一周当中的具体日期是必需参数,但默认温度为 22 度,默认脏污级别为 20。
使用不带参数的when表达式,在 Kotlin 中,该参数充当一系列if/else if检查。
fun shouldChangeWater (day: String, temperature: Int = 22, dirty: Int = 20): Boolean {
return when {
temperature > 30 -> true
dirty > 30 -> true
day == "Sunday" -> true
else -> false
}
}- 从
feedTheFish()中调用shouldChangeWater()并提供具体日期。day参数未设默认值,因此您必须指定一个参数。shouldChangeWater()的其他两个参数设有默认值,因此您无需为其传递实参。
fun feedTheFish() {
val day = randomDay()
val food = fishFood(day)
println ("Today is $day and the fish eat $food")
println("Change water: ${shouldChangeWater(day)}")
}=> Today is Thursday and the fish eat granules Change water: false
第 3 步:创建紧凑型函数
您在上一步中编写的 when 表达式将大量逻辑打包到少量代码中。如果您曾想稍微解压缩,或者要检查的条件更加复杂,您可以使用一些命名合理的局部变量。但是,Kotlin 会利用紧凑型函数实现这一点。
紧凑型函数(即单表达式函数)是 Kotlin 中的一种常见模式。当某个函数返回单个表达式的结果时,您可以在 = 符号后指定该函数的正文,省略大括号 {},并省略 return。
- 在 Hello.kt 中,添加紧凑型函数来测试条件。
fun isTooHot(temperature: Int) = temperature > 30
fun isDirty(dirty: Int) = dirty > 30
fun isSunday(day: String) = day == "Sunday"- 更改
shouldChangeWater()以调用新函数。
fun shouldChangeWater (day: String, temperature: Int = 22, dirty: Int = 20): Boolean {
return when {
isTooHot(temperature) -> true
isDirty(dirty) -> true
isSunday(day) -> true
else -> false
}
}- 运行程序。包含
shouldChangeWater()的println()的输出结果应当与您切换到使用紧凑型函数之前的输出结果相同。
默认值
参数的默认值不必是一个值,可以是另一个函数,如以下部分示例所示:
fun shouldChangeWater (day: String, temperature: Int = 22, dirty: Int = getDirtySensorReading()): Boolean {
...在此任务中,您将了解 Kotlin 中的过滤器。过滤器可以便捷地根据特定条件获取部分列表。
第 1 步:创建过滤器
- 在 Hello.kt 中,使用
listOf()在顶层定义水族箱装饰列表。您可以替换 Hello.kt 的内容。
val decorations = listOf ("rock", "pagoda", "plastic plant", "alligator", "flowerpot")- 创建一个新的
main()函数,其中一行仅输出以字母“p”开头的装饰。过滤条件代码包含在大括号{}中;当过滤器循环遍历时,it会引用每一项。如果表达式返回true,该项将会被包含在内。
fun main() {
println( decorations.filter {it[0] == 'p'})
}- 运行程序,并在 Run 窗口中查看以下输出结果:
⇒ [pagoda, plastic plant]
第 2 步:比较即刻过滤器和延迟过滤器
如果您熟悉其他语言的过滤器,您可能会知道 Kotlin 中的过滤器是即刻过滤器还是延迟过滤器。结果列表是立即创建还是在访问时创建?在 Kotlin 中,可以根据需要确定是即刻过滤器还是延迟过滤器。默认情况下,filter 是即刻过滤器,并且在您每次使用该过滤器时,系统都会创建一个列表。
为使过滤器延迟显示,您可以使用 Sequence,这是一种集合,每次仅支持查看一项内容,从开头开始,一直到其结尾。方便的是,这正是延迟过滤器所需的 API。
- 在 Hello.kt 中,更改代码以将过滤后的列表赋给一个名为
eager的变量,然后输出该变量。
fun main() {
val decorations = listOf ("rock", "pagoda", "plastic plant", "alligator", "flowerpot")
// eager, creates a new list
val eager = decorations.filter { it [0] == 'p' }
println("eager: " + eager)- 在该代码下面,使用包含
asSequence()的Sequence对过滤器执行求值。将序列赋给一个名为filtered的变量,然后输出该变量。
// lazy, will wait until asked to evaluate
val filtered = decorations.asSequence().filter { it[0] == 'p' }
println("filtered: " + filtered)当您以 Sequence 形式返回过滤器结果时,filtered 变量不会保存新列表,而是保存列表元素的 Sequence 以及要应用于这些元素的过滤器信息。每当您访问 Sequence 的元素时,系统就会应用过滤器,并将结果返回给您。
- 使用
toList()将序列转换为List,以强制对该序列执行求值。输出结果。
// force evaluation of the lazy list
val newList = filtered.toList()
println("new list: " + newList)- 运行程序并观察输出结果。
⇒ eager: [pagoda, plastic plant] filtered: kotlin.sequences.FilteringSequence@386cc1c4 new list: [pagoda, plastic plant]
如需直观呈现 Sequence 和延迟求值的情况,请使用 map() 函数。map() 函数会对序列中的每个元素执行简单的转换。
- 仍使用上面的
decorations列表,使用map()执行转换,该函数不执行任何操作且仅返回传递的元素。添加println()以在系统每次访问元素时显示,并将序列赋给一个名为lazyMap的变量。
val lazyMap = decorations.asSequence().map {
println("access: $it")
it
}- 输出
lazyMap,使用first()输出lazyMap的第一个元素,并输出转换为List的lazyMap。
println("lazy: $lazyMap")
println("-----")
println("first: ${lazyMap.first()}")
println("-----")
println("all: ${lazyMap.toList()}")- 运行程序并观察输出结果。输出
lazyMap仅会输出对Sequence的引用,系统不会调用内部println()。输出第一个元素仅会访问第一个元素。将Sequence转换为List可访问所有元素。
⇒ lazy: kotlin.sequences.TransformingSequence@5ba23b66 ----- access: rock first: rock ----- access: rock access: pagoda access: plastic plant access: alligator access: flowerpot all: [rock, pagoda, plastic plant, alligator, flowerpot]
- 使用原始过滤器创建新的
Sequence,然后应用map。输出该结果。
val lazyMap2 = decorations.asSequence().filter {it[0] == 'p'}.map {
println("access: $it")
it
}
println("-----")
println("filtered: ${ lazyMap2.toList() }")- 运行程序并观察其他输出结果。与获取第一个元素一样,系统仅会对访问的元素调用内部
println()。
⇒ ----- access: pagoda access: plastic plant filtered: [pagoda, plastic plant]
在此任务中,您将了解 Kotlin 中的 lambda 和高阶函数。
lambda
除传统命名函数外,Kotlin 还支持 lambda。lambda 是用于创建函数的表达式。但是,您不必声明有名称的函数,只需声明没有名称的函数。这样一来,lambda 表达式现在可作为数据传递。在其他语言中,lambda 称为匿名函数、函数字面量或类似名称。
高阶函数
您可以通过将 lambda 传递给另一个函数来创建高阶函数。在上一个任务中,您创建了一个名为 filter 的高阶函数。您将以下 lambda 表达式传递给 filter,作为要检查的条件:{it[0] == 'p'}
同样,map 是高阶函数,您曾向该函数传递的 lambda 是要应用的转换。
第 1 步:了解 lambda
- 与命名函数一样,lambda 可以具有参数。对于 lambda,参数(及其类型,如果需要)位于所谓的函数箭头
->的左侧。要执行的代码位于该函数箭头的右侧。一旦将 lambda 赋给变量,就可像调用函数一样调用它。
使用 REPL (Tools > Kotlin > Kotlin REPL),试用以下代码:
var dirtyLevel = 20
val waterFilter = { dirty : Int -> dirty / 2}
println(waterFilter(dirtyLevel))⇒ 10
在此示例中,lambda 采用名为 dirty 的 Int,并返回 dirty / 2。(因为过滤可以去除污垢。)
- Kotlin 的函数类型语法与其 lambda 语法密切相关。使用以下语法清晰地声明一个包含函数的变量:
val waterFilter: (Int) -> Int = { dirty -> dirty / 2 }此代码的作用如下:
- 创建一个名为
waterFilter的变量。 waterFilter可以是任何采用Int并返回Int的函数。- 将 lambda 赋给
waterFilter。 - lambda 会返回参数
dirty除以 2 所得到的值。
请注意,您不再需要指定 lambda 参数的类型。此类型根据类型推断计算得出。
第 2 步:创建高阶函数
截至目前为止,在大多数情况下,lambda 的示例看起来与函数类似。lambda 的真正强大之处在于:它们用于创建高阶函数,其中,一个函数的参数是另一个函数。
- 编写一个高阶函数。以下是一个基本示例,即一个采用两个参数的函数。第一个参数是一个整数。第二个参数是一个采用并返回整数的函数。在 REPL 中试用。
fun updateDirty(dirty: Int, operation: (Int) -> Int): Int {
return operation(dirty)
}代码的正文会调用作为第二个参数传递的函数,并向其传递第一个参数。
- 如需调用此函数,请向其传递一个整数和一个函数。
val waterFilter: (Int) -> Int = { dirty -> dirty / 2 }
println(updateDirty(30, waterFilter))⇒ 15
您传递的函数不必是 lambda;相反,此函数可以是一个常规命名函数。如需将该参数指定为常规函数,请使用 :: 运算符。这样,Kotlin 就能知道您将函数引用作为参数传递,而不是尝试调用该函数。
- 尝试将常规命名函数传递给
updateDirty()。
fun increaseDirty( start: Int ) = start + 1
println(updateDirty(15, ::increaseDirty))⇒ 16
var dirtyLevel = 19;
dirtyLevel = updateDirty(dirtyLevel) { dirtyLevel -> dirtyLevel + 23}
println(dirtyLevel)⇒ 42
- 如需在 IntelliJ IDEA 中创建 Kotlin 源文件,请先创建一个 Kotlin 项目。
- 如需在 IntelliJ IDEA 中编译并运行程序,请点击
main()函数旁边的绿色三角形。输出结果会显示在下面的日志窗口中。 - 在 IntelliJ IDEA 中,在 Run > Edit Configurations 中指定要传递给
main()函数的命令行参数。 - Kotlin 中的几乎所有内容都具有值。通过将
if或when的值用作表达式或返回值,您可以利用这一点使代码更加简洁。 - 使用默认参数,便不再需要多个版本的函数或方法。例如:
fun swim(speed: String = "fast") { ... } - 使用紧凑型函数(即单表达式函数)可以提高代码的可读性。例如:
fun isTooHot(temperature: Int) = temperature > 30 - 您已了解一些有关使用 lambda 表达式的过滤器的基础知识。例如:
val beginsWithP = decorations.filter { it [0] == 'p' } - lambda 表达式是一种用于创建无名函数的表达式。lambda 表达式在大括号
{}之间定义。 - 在高阶函数中,您可以将一个函数(如 lambda 表达式)作为数据传递给另一个函数。例如:
dirtyLevel = updateDirty(dirtyLevel) { dirtyLevel -> dirtyLevel + 23}
本课涵盖内容较广,对于不熟悉 lambda 的人来说存在一定难度。后续课程将重新介绍 lambda 和高阶函数。
Kotlin 文档
如果您想详细了解本课程中的任何主题,或者遇到任何问题,不妨先访问 https://kotlinlang.org。
Kotlin 教程
https://try.kotlinlang.org 网站包含丰富的教程(称为 Kotlin Koans)、一个基于网络的解释器以及一套完整的参考文档和示例。
Udacity 课程
如需查看有关此主题的 Udacity 课程,请参阅面向编程人员的 Kotlin 训练营。
IntelliJ IDEA
您可以在 JetBrains 网站上找到 IntelliJ IDEA 的文档。
此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:
- 根据需要布置作业。
- 告知学生如何提交家庭作业。
- 给家庭作业评分。
讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。
如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。
回答以下问题
问题 1
如果字符串 element 包含在调用该函数的字符串中,则 contains(element: String) 函数会返回 true。以下代码的输出是什么?
val decorations = listOf ("rock", "pagoda", "plastic plant", "alligator", "flowerpot")
println(decorations.filter {it.contains('p')})
▢ [pagoda, plastic, plant]
▢ [pagoda, plastic plant]
▢ [pagoda, plastic plant, flowerpot]
▢ [rock, alligator]
问题 2
在以下函数定义中,哪个参数是必需的?fun shouldChangeWater (day: String, temperature: Int = 22, dirty: Int = 20, numDecorations: Int = 0): Boolean {...}
▢ numDecorations
▢ dirty
▢ day
▢ temperature
问题 3
您可以将常规命名函数(而非调用该函数的结果)传递给另一个函数。如何将 increaseDirty( start: Int ) = start + 1 传递给 updateDirty(dirty: Int, operation: (Int) -> Int)?
▢ updateDirty(15, &increaseDirty())
▢ updateDirty(15, increaseDirty())
▢ updateDirty(15, ("increaseDirty()"))
▢ updateDirty(15, ::increaseDirty)
继续学习下一课:
如需查看本课程的概览(包括指向其他 Codelab 的链接),请参阅“面向程序员的 Kotlin 训练营:欢迎来到本课程”。