面向编程人员的 Kotlin 训练营 6:功能操纵

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

简介

这是 Kotlin 训练营中的最后一个 Codelab。在此 Codelab 中,您将了解注解和带标签的 break。您将回顾 lambda 和高阶函数,它们是 Kotlin 的关键部分。您还将详细了解内联函数和单一抽象方法 (SAM) 接口。最后,您将详细了解 Kotlin 标准库

本课程中的各个课程旨在帮助您积累知识,但彼此之间半独立,因此您可以略读自己熟悉的部分,而不是构建单个示例应用。为了将这些示例联系起来,我们使用了水族馆主题。如果您想了解完整的水族馆故事,请查看 Kotlin 编程人员训练营 Udacity 课程。

您应当已掌握的内容

  • Kotlin 函数、类和方法的语法
  • 如何在 IntelliJ IDEA 中创建新类并运行程序
  • lambda 和高阶函数的基础知识

学习内容

  • 注释基础知识
  • 如何使用带标签的中断
  • 详细了解高阶函数
  • 单一抽象方法 (SAM) 接口简介
  • Kotlin 标准库简介

您将执行的操作

  • 创建简单的注释。
  • 使用带标签的中断。
  • 查看 Kotlin 中的 lambda 函数。
  • 使用和创建高阶函数。
  • 调用一些单一抽象方法接口。
  • 使用 Kotlin 标准库中的一些函数。

注解是一种将元数据附加到代码的方式,并非 Kotlin 特有的功能。注释由编译器读取,并用于生成代码或逻辑。许多框架(例如 KtorKotlinx)以及 Room 都使用注释来配置其运行方式以及与代码的互动方式。在开始使用框架之前,您不太可能会遇到任何注释,但了解如何解读注释会很有用。

此外,Kotlin 标准库还提供了一些注解,用于控制代码的编译方式。如果您要将 Kotlin 代码导出到 Java 代码,它们会非常有用,但在其他情况下,您并不需要经常使用它们。

注解位于被注解的对象之前,大多数对象都可以被注解,包括类、函数、方法,甚至控制结构。某些注释可以接受实参。

以下是一些注释的示例。

@file:JvmName("InteropFish")
class InteropFish {
   companion object {
       @JvmStatic fun interop()
   }
}

这表示相应文件的导出名称为 InteropFish,并带有 JvmName 注释;JvmName 注释的实参为 "InteropFish"。在伴生对象中,@JvmStatic 会告知 Kotlin 将 interop() 作为 InteropFish 中的静态函数。

您也可以创建自己的注释,但只有在编写需要在运行时获取类特定信息的库(即反射)时,此功能才最有用。

第 1 步:创建新软件包和文件

  1. src 下,创建一个新软件包 example
  2. example 中,创建一个新的 Kotlin 文件 Annotations.kt

第 2 步:创建自己的注释

  1. Annotations.kt 中,创建一个包含两种方法(trim()fertilize())的 Plant 类。
class Plant {
        fun trim(){}
        fun fertilize(){}
}
  1. 创建一个函数,用于打印类中的所有方法。使用 ::class 在运行时获取有关类的信息。使用 declaredMemberFunctions 获取类的方法列表。(如需访问此功能,您需要导入 kotlin.reflect.full.*
import kotlin.reflect.full.*    // required import

class Plant {
    fun trim(){}
    fun fertilize(){}
}

fun testAnnotations() {
    val classObj = Plant::class
    for (m in classObj.declaredMemberFunctions) {
        println(m.name)
    }
}
  1. 创建一个 main() 函数来调用测试例程。运行程序并观察输出结果。
fun main() {
    testAnnotations()
}
⇒ trim
fertilize
  1. 创建简单的注释,ImAPlant
annotation class ImAPlant

除了说明已添加注释之外,此代码不会执行任何其他操作。

  1. Plant 类前面添加注释。
@ImAPlant class Plant{
    ...
}
  1. testAnnotations() 更改为打印类的所有注释。使用 annotations 获取类的所有注释。运行程序并观察结果。
fun testAnnotations() {
    val plantObject = Plant::class
    for (a in plantObject.annotations) {
        println(a.annotationClass.simpleName)
    }
}
⇒ ImAPlant
  1. 更改 testAnnotations() 以查找 ImAPlant 注释。使用 findAnnotation() 查找特定注释。运行程序并观察结果。
fun testAnnotations() {
    val plantObject = Plant::class
    val myAnnotationObject = plantObject.findAnnotation<ImAPlant>()
    println(myAnnotationObject)
}
⇒ @example.ImAPlant()

第 3 步:创建有针对性的注释

注释可以定位 getter 或 setter。如果它们是,您可以使用 @get:@set: 前缀来应用它们。当使用带注释的框架时,这种情况经常出现。

  1. 声明两个注释,OnGet 只能应用于属性 getter,OnSet 只能应用于属性 setter。在每个设备上使用 @Target(AnnotationTarger.PROPERTY_GETTER)PROPERTY_SETTER
annotation class ImAPlant

@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class OnGet
@Target(AnnotationTarget.PROPERTY_SETTER)
annotation class OnSet

@ImAPlant class Plant {
    @get:OnGet
    val isGrowing: Boolean = true

    @set:OnSet
    var needsFood: Boolean = false
}

注释非常强大,可用于创建在运行时和有时在编译时检查事物的库。不过,典型的应用代码只会使用框架提供的注释。

Kotlin 提供了多种控制流程的方法。您已经熟悉 return,它用于从函数返回到其封装函数。使用 break 类似于 return,但适用于循环。

Kotlin 通过所谓的带标签的 break 语句,让您可以对循环进行额外的控制。带有标签的 break 会跳转到带有相应标签的循环之后的执行点。在处理嵌套循环时,此功能特别有用。

Kotlin 中的任何表达式都可以使用标签进行标记。标签的形式为标识符后跟 @ 符号。

  1. Annotations.kt 中,尝试通过跳出内层循环来使用带标签的 break。
fun labels() {
    outerLoop@ for (i in 1..100) {
         print("$i ")
         for (j in 1..100) {
             if (i > 10) break@outerLoop  // breaks to outer loop
        }
    }
}

fun main() {
    labels()
}
  1. 运行程序并观察输出结果。
⇒ 1 2 3 4 5 6 7 8 9 10 11 

同样,您也可以使用带标签的 continue。标记的 continue 语句不会跳出标记的循环,而是继续执行循环的下一次迭代。

lambda 是匿名函数,即没有名称的函数。您可以将它们分配给变量,并将其作为实参传递给函数和方法。它们非常有用。

第 1 步:创建简单的 Lambda

  1. 在 IntelliJ IDEA 中启动 REPL,依次选择 Tools > Kotlin > Kotlin REPL
  2. 创建一个带实参 dirty: Int 的 lambda,该 lambda 会执行一项计算,将 dirty 除以 2。将 lambda 赋给变量 waterFilter
val waterFilter = { dirty: Int -> dirty / 2 }
  1. 调用 waterFilter,并传入值 30。
waterFilter(30)
⇒ res0: kotlin.Int = 15

第 2 步:创建过滤条件 lambda

  1. 仍在 REPL 中,创建一个包含一个属性 name 的数据类 Fish
data class Fish(val name: String)
  1. 创建一个包含 3 个 Fish 的列表,名称分别为 Flipper、Moby Dick 和 Dory。
val myFish = listOf(Fish("Flipper"), Fish("Moby Dick"), Fish("Dory"))
  1. 添加一个过滤条件,以检查包含字母“i”的名称。
myFish.filter { it.name.contains("i")}
⇒ res3: kotlin.collections.List<Line_1.Fish> = [Fish(name=Flipper), Fish(name=Moby Dick)]

在 lambda 表达式中,it 指的是当前列表元素,并且过滤器会依次应用于每个列表元素。

  1. 使用 ", " 作为分隔符,将 joinString() 应用于结果。
myFish.filter { it.name.contains("i")}.joinToString(", ") { it.name }
⇒ res4: kotlin.String = Flipper, Moby Dick

joinToString() 函数通过联接过滤后的名称来创建字符串,并以指定的字符串作为分隔符。它是 Kotlin 标准库中内置的众多实用函数之一。

将 lambda 或其他函数作为实参传递给函数会创建高阶函数。上面的过滤条件就是一个简单的示例。filter() 是一个函数,您需要向其传递一个 lambda,以指定如何处理列表中的每个元素。

使用扩展 lambda 编写高阶函数是 Kotlin 语言中最先进的部分之一。虽然需要一段时间才能学会如何编写它们,但使用起来非常方便。

第 1 步:创建新类

  1. example 软件包中,创建一个新的 Kotlin 文件 Fish.kt
  2. Fish.kt 中,创建一个数据类 Fish,其中包含一个属性 name
data class Fish (var name: String)
  1. 创建函数 fishExamples()。在 fishExamples() 中,创建一个名为 "splashy" 的鱼(全部小写)。
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
}
  1. 创建一个调用 fishExamples()main() 函数。
fun main () {
    fishExamples()
}
  1. 点击 main() 左侧的绿色三角形,编译并运行程序。目前还没有输出。

第 2 步:使用高阶函数

借助 with() 函数,您可以更紧凑地引用对象或属性。使用 thiswith() 实际上是一个高阶函数,您可以在 lambda 中指定如何处理提供的对象。

  1. 使用 with()fishExamples() 中的鱼名首字母大写。在花括号内,this 指的是传递给 with() 的对象。
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        this.capitalize()
    }
}
  1. 没有输出,因此请在周围添加 println()this 是隐式的,不需要,因此您可以将其移除。
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        println(capitalize())
    }
}
⇒ Splashy

第 3 步:创建高阶函数

在底层,with() 是一个高阶函数。如需了解其运作方式,您可以自行制作一个仅适用于字符串的 with() 大大简化版本。

  1. Fish.kt 中,定义一个接受两个实参的函数 myWith()。实参是要操作的对象,以及定义操作的函数。函数的实参名称惯例为 block。在这种情况下,该函数不返回任何内容,这由 Unit 指定。
fun myWith(name: String, block: String.() -> Unit) {}

myWith() 中,block() 现在是 String 的扩展函数。被扩展的类通常称为接收器对象。因此,在本例中,name 是接收器对象。

  1. myWith() 的正文中,将传入的函数 block() 应用于接收器对象 name
fun myWith(name: String, block: String.() -> Unit) {
    name.block()
}
  1. fishExamples() 中,将 with() 替换为 myWith()
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    myWith (fish.name) {
        println(capitalize())
    }
}

fish.name 是名称实参,println(capitalize()) 是代码块函数。

  1. 运行该程序,它会像以前一样运行。
⇒ Splashy

第 4 步:探索更多内置扩展服务

with() 扩展 lambda 非常有用,是 Kotlin 标准库的一部分。以下是一些您可能会觉得有用的其他功能:run()apply()let()

run() 函数是一种可与所有类型搭配使用的扩展函数。它接受一个 lambda 作为实参,并返回执行该 lambda 的结果。

  1. fishExamples() 中,对 fish 调用 run() 以获取名称。
fish.run {
   name
}

此方法仅返回 name 属性。您可以将该值赋给变量或将其输出。这实际上并不是一个有用的示例,因为您只需访问该属性即可,但 run() 对于更复杂的表达式可能很有用。

apply() 函数与 run() 类似,但它会返回应用到的已更改对象,而不是 lambda 的结果。这对于对新创建的对象调用方法非常有用。

  1. 复制 fish 并调用 apply() 以设置新副本的名称。
val fish2 = Fish(name = "splashy").apply {
     name = "sharky"
}
println(fish2.name)
⇒ sharky

let() 函数与 apply() 类似,但它会返回经过更改的对象副本。这对于将多个操作串联在一起非常有用。

  1. 使用 let() 获取 fish 的名称,将其大写,将另一个字符串与其连接,获取该结果的长度,将该长度加 31,然后打印结果。
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})
⇒ 42

在此示例中,it 所指的对象类型依次为 FishStringStringInt

  1. 在调用 let() 后打印 fish,您会发现它没有变化。
println(fish.let { it.name.capitalize()}
    .let{it + "fish"}
    .let{it.length}
    .let{it + 31})
println(fish)
⇒ 42
Fish(name=splashy)

lambda 和高阶函数非常有用,但您应该了解一点:lambda 是对象。lambda 表达式是 Function 接口的实例,而该接口本身是 Object 的子类型。以之前的 myWith() 示例为例。

myWith(fish.name) {
    capitalize()
}

Function 接口有一个方法 invoke(),该方法会被替换以调用 lambda 表达式。如果写成完整形式,则如下面的代码所示。

// actually creates an object that looks like this
myWith(fish.name, object : Function1<String, Unit> {
    override fun invoke(name: String) {
        name.capitalize()
    }
})

通常,这不会造成问题,因为创建对象和调用函数不会产生太多开销(即内存和 CPU 时间)。但如果您要定义像 myWith() 这样在各处都使用的内容,开销可能会累积起来。

Kotlin 提供了 inline 来处理这种情况,通过为编译器增加少量工作来减少运行时的开销。(在之前介绍具体化类型的课程中,您已经对 inline 有了初步了解。)将函数标记为 inline 意味着,每次调用该函数时,编译器实际上都会转换源代码以“内联”该函数。也就是说,编译器会更改代码,将 lambda 替换为 lambda 中的指令。

如果上述示例中的 myWith() 标记为 inline

inline myWith(fish.name) {
    capitalize()
}

它会转换为直接调用:

// with myWith() inline, this becomes
fish.name.capitalize()

值得注意的是,内联大型函数会增加代码大小,因此最好仅将此功能用于多次使用的简单函数,例如 myWith()。您之前了解的库中的扩展函数标记为 inline,因此您不必担心会创建额外的对象。

单一抽象方法是指接口上只有一个方法。在使用 Java 编程语言编写的 API 时,它们非常常见,因此有一个缩写名称,即 SAM。例如,Runnable 具有单一抽象方法 run(),而 Callable 具有单一抽象方法 call()

在 Kotlin 中,您必须始终调用将 SAM 作为参数的函数。试试下面的示例。

  1. example 中,创建一个 Java 类 JavaRun,并将以下内容粘贴到该文件中。
package example;

public class JavaRun {
    public static void runNow(Runnable runnable) {
        runnable.run();
    }
}

在 Kotlin 中,您可以在类型前面加上 object:,以实例化实现接口的对象。此方法可用于向 SAM 传递参数。

  1. 返回到 Fish.kt,创建一个函数 runExample(),该函数使用 object: 创建 Runnable。该对象应通过打印 "I'm a Runnable" 来实现 run()
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
}
  1. 使用您创建的对象调用 JavaRun.runNow()
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
    JavaRun.runNow(runnable)
}
  1. main() 调用 runExample() 并运行程序。
⇒ I'm a Runnable

虽然打印内容需要做很多工作,但这是 SAM 工作方式的一个很好的示例。当然,Kotlin 提供了一种更简单的方法来实现此目的,即使用 lambda 代替对象,从而使此代码更加紧凑。

  1. 移除 runExample 中的现有代码,将其更改为使用 lambda 调用 runNow(),然后运行该程序。
fun runExample() {
    JavaRun.runNow({
        println("Passing a lambda as a Runnable")
    })
}
⇒ Passing a lambda as a Runnable
  1. 您可以使用最后一个参数的调用语法,使代码更加简洁,并摆脱圆括号。
fun runExample() {
    JavaRun.runNow {
        println("Last parameter is a lambda as a Runnable")
    }
}
⇒ Last parameter is a lambda as a Runnable

这就是 SAM(单一抽象方法)的基础知识。您可以使用以下模式,通过一行代码实例化、替换和调用 SAM:
Class.singleAbstractMethod { lambda_of_override }

本课回顾了 lambda,并更深入地介绍了 Kotlin 的关键部分 - 高阶函数。您还了解了注释和带标签的 break。

  • 使用注释向编译器指定内容。例如:
    @file:JvmName("Foo")
  • 使用带标签的 break 语句,让代码从嵌套循环内部退出。例如:
    if (i > 10) break@outerLoop // breaks to outerLoop label
  • lambda 与高阶函数搭配使用时,功能非常强大。
  • Lambda 是对象。为避免创建对象,您可以使用 inline 标记函数,这样编译器会将 lambda 的内容直接放在代码中。
  • 请谨慎使用 inline,但它有助于减少程序的资源使用量。
  • SAM(单一抽象方法)是一种常见模式,使用 lambda 可简化此模式。基本模式为:
    Class.singleAbstractMethod { lamba_of_override }
  • Kotlin 标准库提供了许多实用函数,包括多个 SAM,因此请了解其中的内容。

Kotlin 的功能远不止本课程中介绍的这些,但您现在已经掌握了开始开发自己的 Kotlin 程序所需的基础知识。希望您对这种富有表现力的语言感到兴奋,并期待在编写更少代码的同时创建更多功能(如果您之前使用的是 Java 编程语言,这一点尤其重要)。边实践边学习是成为 Kotlin 专家的最佳方式,因此请继续自行探索和学习 Kotlin。

Kotlin 文档

如果您想详细了解本课程中的任何主题,或者遇到任何问题,不妨先访问 https://kotlinlang.org

Kotlin 教程

https://try.kotlinlang.org 网站包含丰富的教程(称为 Kotlin Koans)、一个基于网络的解释器以及一套完整的参考文档和示例。

Udacity 课程

如需查看有关此主题的 Udacity 课程,请参阅面向编程人员的 Kotlin 训练营

IntelliJ IDEA

您可以在 JetBrains 网站上找到 IntelliJ IDEA 的文档

Kotlin 标准库

Kotlin 标准库提供了许多实用函数。在编写自己的函数或接口之前,请务必先检查标准库,看看是否有人已经为您节省了一些工作。我们会经常添加新功能,请不时回来看看。

Kotlin 教程

https://try.kotlinlang.org 网站包含丰富的教程(称为 Kotlin Koans)、一个基于网络的解释器以及一套完整的参考文档和示例。

Udacity 课程

如需查看有关此主题的 Udacity 课程,请参阅面向编程人员的 Kotlin 训练营

IntelliJ IDEA

您可以在 JetBrains 网站上找到 IntelliJ IDEA 的文档

此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:

  • 根据需要布置作业。
  • 告知学生如何提交家庭作业。
  • 给家庭作业评分。

讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。

如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。

回答以下问题

问题 1

在 Kotlin 中,SAM 是指:

▢ 安全的实参匹配

▢ 简单访问方法

▢ 单个抽象方法

▢ 战略性访问方法

问题 2

以下哪项不是 Kotlin 标准库扩展函数?

elvis()

apply()

run()

with()

问题 3

以下哪项关于 Kotlin 中 lambda 的陈述是不正确的?

▢ Lambda 是匿名函数。

▢ Lambda 是对象,除非内联。

▢ Lambda 属于资源密集型,不应使用。

▢ Lambda 可以传递给其他函数。

问题 4

Kotlin 中的标签通过标识符后跟以下内容来表示:

:

::

@:

@

恭喜!您已完成“面向编程人员的 Kotlin 训练营”Codelab。

如需查看本课程的概览(包括指向其他 Codelab 的链接),请参阅“面向程序员的 Kotlin 训练营:欢迎来到本课程”