面向编程人员 3 的 Kotlin 训练营:函数

此 Codelab 是面向编程人员的 Kotlin 训练营课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。根据您的知识水平,您也许可以浏览某些部分。本课程专为了解面向对象的语言并希望学习 Kotlin 的编程人员而设计。

简介

在此 Codelab 中,您将创建一个 Kotlin 程序并了解 Kotlin 中的函数,包括参数默认值、过滤器、lambda 和紧凑函数。

本课程不是构建单个示例应用,而是用来学习知识,但它们是半独立的,以便您能够浏览已熟悉的部分。为了将两者联系起来,许多示例使用了水族馆主题。如果您想看完整的水族馆故事,请查看面向编程人员的 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 文件

  1. 打开 IntelliJ IDEA。
  2. IntelliJ IDEA 左侧的 Project 窗格显示项目文件和文件夹列表。找到并右键点击 Hello Kotlin 下的 src 文件夹。(您应该已经拥有上一个 Codelab 中的 Hello Kotlin 项目。)
  3. 依次选择 New > Kotlin File / Class
  4. 保留种类File,并将文件命名为 Hello
  5. 点击 OK

现在,src 文件夹中有一个名为 Hello.kt 的文件。

第 2 步:添加代码并运行程序

  1. 与其他语言一样,Kotlin main() 函数指定了执行的入口点。任何命令行参数都以字符串数组的形式传递。

    将以下代码输入或粘贴到 Hello.kt 文件中:
fun main(args: Array<String>) {
    println("Hello, world!")
}

与您之前的 printHello() 函数一样,此函数没有 return 语句。Kotlin 中的每个函数都会返回一些内容,即使没有明确指定任何内容也是如此。因此,此类 main() 函数会返回 kotlin.Unit 类型,这是 Kotlin 声明没有值的方式。

  1. 如需运行程序,请点击 main() 函数左侧的绿色三角形。从菜单中选择 Run 'HelloKt'
  2. IntelliJ IDEA 会编译并运行该程序。结果会显示在底部的日志窗格中,如下所示。

第 3 步:向 main() 传递参数

由于您从 IntelliJ IDEA 而非命令行来运行程序,因此需要为该程序指定任何略有不同的参数。

  1. 依次选择 Run > Edit Configurations。系统会显示 Run/Debug Configurations 窗口。
  2. Program arguments 字段中输入 Kotlin!
  3. 点击 OK

第 4 步:更改代码以使用字符串模板

字符串模板会在字符串中插入变量或表达式,而 $ 则指定字符串的一部分是变量或表达式。大括号 {} 用于括住表达式(如有)。

  1. Hello.kt 中,更改问候语以使用传递到程序的第一个参数 args[0],而不是 "world"
fun main(args: Array<String>) {
    println("Hello, ${args[0]}")
}
  1. 运行程序,输出结果将包含您指定的参数。
⇒ Hello, Kotlin!

在此任务中,您将了解 Kotlin 中的几乎所有内容都具有值的原因,及其具有重要意义的原因。

还有一些语言则具有语句,这些语句是没有值的代码行。在 Kotlin 中,几乎所有内容都是表达式,并且都具有值(即使该值为 kotlin.Unit)。

  1. Hello.kt 中,使用 main() 编写代码,为名为 isUnit 的变量分配 println() 并输出该变量。(println() 不返回任何值,因此它返回 kotlin.Unit。)
// Will assign kotlin.Unit
val isUnit = println("This is an expression")
println(isUnit)
  1. 运行程序。第一个 println() 会输出字符串 "This is an expression"。第二个 println() 会输出第一个 println() 语句的值,即 kotlin.Unit
⇒ This is an expression
kotlin.Unit
  1. 声明一个名为 temperatureval,并将其初始化为 10。
  2. 声明另一个名为 isHotval,并为 isHot 赋予 if/else 语句的返回值,如以下代码所示。由于它是一个表达式,因此您可以立即使用 if 表达式的值。
val temperature = 10
val isHot = if (temperature > 50) true else false
println(isHot)
⇒ false
  1. 在字符串模板中使用表达式的值。添加一些代码来检查温度以确定鱼类是否安全或过热,然后运行程序。
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 的内容。

  1. 编写一个名为 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()
}
  1. 编写 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)]
}
  1. Random()nextInt() 函数在 java.util.* 中定义。在文件顶部添加所需导入:
import java.util.*    // required import
  1. 运行程序并检查输出结果。
⇒ Today is Tuesday and the fish eat pellets

第 2 步:使用 when 表达式

进一步扩展,更改代码以使用 when 表达式为不同日期选择不同食物。在其他编程语言中,when 语句类似于 switch,但 when 会在每个分支结束时自动中断。在您检查枚举的情况下,它还可确保您的代码覆盖所有分支。

  1. 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
  1. 使用 elsewhen 表达式添加默认分支。为进行测试,为了确保在程序中有时会采用默认值,请移除 TuesdaySaturday 分支。

    拥有默认分支可确保 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
}
  1. 由于每个表达式都具有值,因此您可以使此代码更简洁一些。直接返回 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 中,您可以按参数名称传递参数。此外,您还可以为参数指定默认值:如果调用方未提供参数,系统会使用默认值。稍后,当您编写方法(成员函数)时,这意味着您可以避免编写同一方法的大量重载版本。

  1. Hello.kt 中,编写一个 swim() 函数,其中包含一个名为 speedString 参数,用于输出鱼类的速度。speed 参数的默认值为 "fast"
fun swim(speed: String = "fast") {
   println("swimming $speed")
}
  1. 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 步:添加必需参数

如果未为参数指定默认值,必须始终传递相应的参数。

  1. Hello.kt 中,编写一个 shouldChangeWater() 函数,该函数采用三个参数:daytemperaturedirty 级别。如果应当更换水,该函数会返回 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
    }
}
  1. 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

  1. Hello.kt 中,添加紧凑型函数来测试条件。
fun isTooHot(temperature: Int) = temperature > 30

fun isDirty(dirty: Int) = dirty > 30

fun isSunday(day: String) = day == "Sunday"
  1. 更改 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
    }
}
  1. 运行程序。包含 shouldChangeWater()println() 的输出结果应当与您切换到使用紧凑型函数之前的输出结果相同。

默认值

参数的默认值不必是一个值,它也可以是另一个函数,如以下部分示例所示:

fun shouldChangeWater (day: String, temperature: Int = 22, dirty: Int = getDirtySensorReading()): Boolean {
    ...

在此任务中,您将了解 Kotlin 中的过滤器。过滤器可以便捷地根据特定条件获取部分列表。

第 1 步:创建过滤器

  1. Hello.kt 中,使用 listOf() 在顶层定义水族箱装饰列表。您可以替换 Hello.kt 的内容。
val decorations = listOf ("rock", "pagoda", "plastic plant", "alligator", "flowerpot")
  1. 使用一行代码创建一个新的 main() 函数,以仅输出以字母 # 开头的装饰。过滤条件的代码是用大括号 {} 表示的,it 表示过滤器遍历时的各项内容。如果表达式返回 true,该项将会被包含在内。
fun main() {
    println( decorations.filter {it[0] == 'p'})
}
  1. 运行程序,并在 Run 窗口中查看以下输出结果:
⇒ [pagoda, plastic plant]

第 2 步:比较即刻过滤器和延迟过滤器

如果您熟悉其他语言的过滤器,您可能会知道 Kotlin 中的过滤器是即刻过滤器还是延迟过滤器。是立即创建结果列表,还是在访问结果列表时创建结果列表?在 Kotlin 中,可以根据需要确定是即刻过滤器还是延迟过滤器。默认情况下,filter 是即刻过滤器,并且在您每次使用该过滤器时,系统都会创建一个列表。

要让过滤器延迟执行,您可以使用 Sequence,它是一个集合,每次只能查看 1 项内容,且从头至尾都返回。方便的是,这正是延迟过滤器所需的 API。

  1. 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)
  1. 在该代码下面,使用包含 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 的元素时,系统就会应用过滤器,并将结果返回给您。

  1. 使用 toList() 将序列转换为 List,以强制对该序列执行求值。输出结果。
    // force evaluation of the lazy list
    val newList = filtered.toList()
    println("new list: " + newList)
  1. 运行程序并观察输出结果。
⇒ eager: [pagoda, plastic plant]
filtered: kotlin.sequences.FilteringSequence@386cc1c4
new list: [pagoda, plastic plant]

如需直观呈现 Sequence 和延迟求值的情况,请使用 map() 函数。map() 函数会对序列中的每个元素执行简单的转换。

  1. 仍使用上面的 decorations 列表,使用 map() 执行转换,该函数不执行任何操作且仅返回传递的元素。添加 println() 以在系统每次访问元素时显示,并将序列赋给一个名为 lazyMap 的变量。
    val lazyMap = decorations.asSequence().map {
        println("access: $it")
        it
    }
  1. 输出 lazyMap,使用 first() 输出 lazyMap 的第一个元素,并输出转换为 ListlazyMap
    println("lazy: $lazyMap")
    println("-----")
    println("first: ${lazyMap.first()}")
    println("-----")
    println("all: ${lazyMap.toList()}")
  1. 运行程序并观察输出结果。输出 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]
  1. 使用原始过滤器创建新的 Sequence,然后应用 map。输出该结果。
    val lazyMap2 = decorations.asSequence().filter {it[0] == 'p'}.map {
        println("access: $it")
        it
    }
    println("-----")
    println("filtered: ${ lazyMap2.toList() }")
  1. 运行程序并观察其他输出结果。与获取第一个元素一样,系统仅会对访问的元素调用内部 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

  1. 与命名函数一样,lambda 可以具有参数。对于 lambda,参数(及其类型,如果需要)位于所谓的函数箭头 -> 的左侧。要执行的代码位于该函数箭头的右侧。将 lambda 分配给变量后,您可以像调用函数一样调用它。

    使用 REPL(Tools > Kotlin > Kotlin REPL),试用以下代码:
var dirtyLevel = 20
val waterFilter = { dirty : Int -> dirty / 2}
println(waterFilter(dirtyLevel))
⇒ 10

在此示例中,lambda 采用名为 dirtyInt,并返回 dirty / 2。(因为过滤会清除脏污。)

  1. Kotlin 的函数类型语法与其 lambda 语法密切相关。使用以下语法清晰地声明一个包含函数的变量:
val waterFilter: (Int) -> Int = { dirty -> dirty / 2 }

此代码的作用如下:

  • 创建一个名为 waterFilter 的变量。
  • waterFilter 可以是任何采用 Int 并返回 Int 的函数。
  • 将 lambda 赋给 waterFilter
  • lambda 会返回参数 dirty 除以 2 所得到的值。

请注意,您不再需要指定 lambda 参数的类型。此类型根据类型推断计算得出。

第 2 步:创建高阶函数

截至目前为止,在大多数情况下,lambda 的示例看起来与函数类似。lambda 的真正强大之处在于:它们用于创建高阶函数,其中,一个函数的参数是另一个函数。

  1. 编写一个高阶函数。以下是一个基本示例,即一个采用两个参数的函数。第一个参数是一个整数。第二个参数是一个采用并返回整数的函数。在 REPL 中试用。
fun updateDirty(dirty: Int, operation: (Int) -> Int): Int {
   return operation(dirty)
}

代码的正文会调用作为第二个参数传递的函数,并向其传递第一个参数。

  1. 如需调用此函数,请向其传递一个整数和一个函数。
val waterFilter: (Int) -> Int = { dirty -> dirty / 2 }
println(updateDirty(30, waterFilter))
⇒ 15

您传递的函数不必是 lambda;相反,此函数可以是一个常规命名函数。如需将该参数指定为常规函数,请使用 :: 运算符。这样,Kotlin 就能知道您将函数引用作为参数传递,而不是尝试调用该函数。

  1. 尝试将常规命名函数传递给 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 中的几乎所有内容都具有值。通过将 ifwhen 的值用作表达式或返回值,您可以利用这一点使代码更加简洁。
  • 使用默认参数,便不再需要多个版本的函数或方法。例如:
    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

您可以将常规命名函数(而不是调用该函数的结果)传递给另一个函数。您希望如何向 updateDirty(dirty: Int, operation: (Int) -> Int) 传递 increaseDirty( start: Int ) = start + 1

updateDirty(15, &increaseDirty())

updateDirty(15, increaseDirty())

updateDirty(15, ("increaseDirty()"))

updateDirty(15, ::increaseDirty)

继续学习下一课:4. 类和对象

如需大致了解本课程(包括指向其他 Codelab 的链接),请参阅面向编程人员的 Kotlin 训练营:欢迎学习本课程