函数

在此任务中,您将创建一个 Kotlin 程序并了解 main() 函数。此外,您还将了解如何从命令行向程序传递参数。

您可能还记得在上一个 Codelab 中输入到 REPL 中的 printHello() 函数:

fun printHello() {
    println ("Hello World")
}

printHello()
⇒ Hello World

如需定义函数,请使用 fun 关键字,后跟函数名称。与其他编程语言一样,圆括号 () 用于函数参数(如有)。大括号 {} 用于括住函数的代码块,并提供作用域限定上下文。printHello() 函数不存在任何返回值类型,因为此函数不会返回任何内容。

第 1 步:创建 Kotlin 文件

  1. 打开 IntelliJ IDEA。
  2. IntelliJ IDEA 左侧的 Project 窗口会显示项目文件和文件夹列表。查找并右键点击 Hello Kotlin 下的 src 文件夹(根据您的 IDE 版本,您可能需要右键点击 src/main 下的 kotlin 文件夹)您应当已从上一个 Codelab 获得 Hello Kotlin 项目。
  3. 依次选择 New > Kotlin File
  4. 将文件命名为 Hello
  5. 点击 OK

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

ebfb3ba0929f45bb.png

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

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

将以下代码输入或粘贴到 Hello.kt 文件中:

fun main(args: Array<String>) {
    println("Hello, world!")
}

与之前的 printHello() 函数一样,此函数没有 return 语句。在 Kotlin 中,即使没有明确指定,每个函数也会返回一些内容。当某个函数没有显式 return 语句时,该函数会返回 Unit(以前为 kotlin.Unit)。因此,没有显式 return 语句的 main() 函数会返回 Unit。

  1. 如需运行程序,请点击 main() 函数左侧的绿色三角形。从菜单中选择 Run 'HelloKt'

IntelliJ IDEA 会编译并运行该程序。结果会显示在底部的日志中,如下所示。

7b8750c0e4817c3d.png

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

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

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

7224905bcfca70c2.png

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

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

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

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

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

注意:与其他编程语言类似,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. Hello.kt 文件的 main() 函数中,声明一个名为 temperatureval 并将其初始化为 10。
  2. 声明另一个名为 isHotval,并为 isHot 赋予 if/else 语句的返回值,如以下代码所示。由于它是一个表达式,因此您可以立即使用 if 表达式的值。
val temperature = 10
val isHot = if (temperature > 50) true else false
println(isHot)

输出结果将显示以下内容:

false
  1. 在字符串模板中使用表达式的值。在 main() 中添加某个代码以检查水温,从而确定水温是否适宜鱼类生存或过热,然后运行程序。
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. Hello.kt 中添加 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() 函数,其中一行仅输出以字母“p”开头的装饰。过滤条件代码包含在大括号 {} 中;当过滤器循环遍历列表时,it 会隐式引用每一项。如果表达式返回 true,该项将会被包含在内。
fun main() {
    println( decorations.filter {it[0] == 'p'})
}
  1. 运行程序,并在 Run 窗口中查看以下输出结果:
⇒ [pagoda, plastic plant]

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

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

为使过滤器延迟显示,您可以使用 Sequence,这是一种集合,每次仅支持查看一项内容,从开头开始,一直到其结尾。方便的是,这正是延迟过滤器所需的 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]
  1. Kotlin 集合的另一个有用的转换函数是 flatten()。此函数从一系列数组或一系列列表等一系列集合创建列表。
  2. 创建一系列列表。然后,应用 flatten() 函数,将所有列表转换为一个列表。输出结果。
val mysports = listOf("basketball", "fishing", "running")
val myplayers = listOf("LeBron James", "Ernest Hemingway", "Usain Bolt")
val mycities = listOf("Los Angeles", "Chicago", "Jamaica")
val mylist = listOf(mysports, myplayers, mycities)     // list of lists
println("-----")
println("Flat: ${mylist.flatten()}")
⇒
-----
Flat: [basketball, fishing, running, LeBron James, Ernest Hemingway, Usain Bolt, Los Angeles, Chicago, Jamaica]

在此任务中,您将了解 Kotlin 中的 lambda 和高阶函数

lambda

除传统命名函数外,Kotlin 还支持与 Java 类似的 lambda。lambda 是描述函数的表达式。但是,您不必声明有名称的函数,只需声明没有名称的函数。这样一来,lambda 表达式现在可作为数据传递。在其他语言中,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 的真正强大之处在于:它们用于创建高阶函数,其中,一个函数的参数是另一个函数。

高阶函数是将其他函数作为参数的函数,或者是返回不同函数的函数。您可以将 lambda 传递给将函数作为参数的高阶函数。在上一个任务中,您创建了一个名为过滤器的高阶函数。此外,您还传递了以下 lambda 表达式,以作为要检查的条件进行过滤:

{ it[0] == 'p' }

同样,map 是高阶函数,您曾向该函数传递的 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 源文件,请点击“Project”窗格中的“src”,然后右键点击以打开菜单。依次选择“New->Kotlin File/Class”。
  • 如需在 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 和高阶函数。