此 Codelab 是面向编程人员的 Kotlin 训练营课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。根据您的知识水平,对于某些版块您也许只需要略读即可。本课程专为了解面向对象的语言并想学习 Kotlin 的编程人员而设计。
简介
这是 Kotlin 训练营中的最后一个 Codelab。在此 Codelab 中,您将了解注解和带标签的 break。您将回顾 lambda 和高阶函数,它们是 Kotlin 的关键部分。您还将详细了解内联函数和单一抽象方法 (SAM) 接口。最后,您将详细了解 Kotlin 标准库。
本课程中的各个课程旨在帮助您积累知识,但彼此之间半独立,因此您可以略读自己熟悉的部分,而不是构建单个示例应用。为了将这些示例联系起来,我们使用了水族馆主题。如果您想了解完整的水族馆故事,请查看 Kotlin 编程人员训练营 Udacity 课程。
您应当已掌握的内容
- Kotlin 函数、类和方法的语法
- 如何在 IntelliJ IDEA 中创建新类并运行程序
- lambda 和高阶函数的基础知识
学习内容
- 注释基础知识
- 如何使用带标签的中断
- 详细了解高阶函数
- 单一抽象方法 (SAM) 接口简介
- Kotlin 标准库简介
您将执行的操作
- 创建简单的注释。
- 使用带标签的中断。
- 查看 Kotlin 中的 lambda 函数。
- 使用和创建高阶函数。
- 调用一些单一抽象方法接口。
- 使用 Kotlin 标准库中的一些函数。
注解是一种将元数据附加到代码的方式,并非 Kotlin 特有的功能。注释由编译器读取,并用于生成代码或逻辑。许多框架(例如 Ktor 和 Kotlinx)以及 Room 都使用注释来配置其运行方式以及与代码的互动方式。在开始使用框架之前,您不太可能会遇到任何注释,但了解如何解读注释会很有用。
此外,Kotlin 标准库还提供了一些注解,用于控制代码的编译方式。如果您要将 Kotlin 代码导出到 Java 代码,它们会非常有用,但在其他情况下,您并不需要经常使用它们。
注解位于被注解的对象之前,大多数对象都可以被注解,包括类、函数、方法,甚至控制结构。某些注释可以接受实参。
以下是一些注释的示例。
@file:JvmName("InteropFish")
class InteropFish {
companion object {
@JvmStatic fun interop()
}
}这表示相应文件的导出名称为 InteropFish,并带有 JvmName 注释;JvmName 注释的实参为 "InteropFish"。在伴生对象中,@JvmStatic 会告知 Kotlin 将 interop() 作为 InteropFish 中的静态函数。
您也可以创建自己的注释,但只有在编写需要在运行时获取类特定信息的库(即反射)时,此功能才最有用。
第 1 步:创建新软件包和文件
- 在 src 下,创建一个新软件包
example。 - 在 example 中,创建一个新的 Kotlin 文件
Annotations.kt。
第 2 步:创建自己的注释
- 在
Annotations.kt中,创建一个包含两种方法(trim()和fertilize())的Plant类。
class Plant {
fun trim(){}
fun fertilize(){}
}- 创建一个函数,用于打印类中的所有方法。使用
::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)
}
}- 创建一个
main()函数来调用测试例程。运行程序并观察输出结果。
fun main() {
testAnnotations()
}⇒ trim fertilize
- 创建简单的注释,
ImAPlant。
annotation class ImAPlant除了说明已添加注释之外,此代码不会执行任何其他操作。
- 在
Plant类前面添加注释。
@ImAPlant class Plant{
...
}- 将
testAnnotations()更改为打印类的所有注释。使用annotations获取类的所有注释。运行程序并观察结果。
fun testAnnotations() {
val plantObject = Plant::class
for (a in plantObject.annotations) {
println(a.annotationClass.simpleName)
}
}⇒ ImAPlant
- 更改
testAnnotations()以查找ImAPlant注释。使用findAnnotation()查找特定注释。运行程序并观察结果。
fun testAnnotations() {
val plantObject = Plant::class
val myAnnotationObject = plantObject.findAnnotation<ImAPlant>()
println(myAnnotationObject)
}
⇒ @example.ImAPlant()
第 3 步:创建有针对性的注释
注释可以定位 getter 或 setter。如果它们是,您可以使用 @get: 或 @set: 前缀来应用它们。当使用带注释的框架时,这种情况经常出现。
- 声明两个注释,
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 中的任何表达式都可以使用标签进行标记。标签的形式为标识符后跟 @ 符号。
- 在
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 2 3 4 5 6 7 8 9 10 11
同样,您也可以使用带标签的 continue。标记的 continue 语句不会跳出标记的循环,而是继续执行循环的下一次迭代。
lambda 是匿名函数,即没有名称的函数。您可以将它们分配给变量,并将其作为实参传递给函数和方法。它们非常有用。
第 1 步:创建简单的 Lambda
- 在 IntelliJ IDEA 中启动 REPL,依次选择 Tools > Kotlin > Kotlin REPL。
- 创建一个带实参
dirty: Int的 lambda,该 lambda 会执行一项计算,将dirty除以 2。将 lambda 赋给变量waterFilter。
val waterFilter = { dirty: Int -> dirty / 2 }- 调用
waterFilter,并传入值 30。
waterFilter(30)⇒ res0: kotlin.Int = 15
第 2 步:创建过滤条件 lambda
- 仍在 REPL 中,创建一个包含一个属性
name的数据类Fish。
data class Fish(val name: String)- 创建一个包含 3 个
Fish的列表,名称分别为 Flipper、Moby Dick 和 Dory。
val myFish = listOf(Fish("Flipper"), Fish("Moby Dick"), Fish("Dory"))- 添加一个过滤条件,以检查包含字母“i”的名称。
myFish.filter { it.name.contains("i")}
⇒ res3: kotlin.collections.List<Line_1.Fish> = [Fish(name=Flipper), Fish(name=Moby Dick)]
在 lambda 表达式中,it 指的是当前列表元素,并且过滤器会依次应用于每个列表元素。
- 使用
", "作为分隔符,将joinString()应用于结果。
myFish.filter { it.name.contains("i")}.joinToString(", ") { it.name }
⇒ res4: kotlin.String = Flipper, Moby Dick
joinToString() 函数通过联接过滤后的名称来创建字符串,并以指定的字符串作为分隔符。它是 Kotlin 标准库中内置的众多实用函数之一。
将 lambda 或其他函数作为实参传递给函数会创建高阶函数。上面的过滤条件就是一个简单的示例。filter() 是一个函数,您需要向其传递一个 lambda,以指定如何处理列表中的每个元素。
使用扩展 lambda 编写高阶函数是 Kotlin 语言中最先进的部分之一。虽然需要一段时间才能学会如何编写它们,但使用起来非常方便。
第 1 步:创建新类
- 在 example 软件包中,创建一个新的 Kotlin 文件
Fish.kt。 - 在
Fish.kt中,创建一个数据类Fish,其中包含一个属性name。
data class Fish (var name: String)- 创建函数
fishExamples()。在fishExamples()中,创建一个名为"splashy"的鱼(全部小写)。
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
}- 创建一个调用
fishExamples()的main()函数。
fun main () {
fishExamples()
}- 点击
main()左侧的绿色三角形,编译并运行程序。目前还没有输出。
第 2 步:使用高阶函数
借助 with() 函数,您可以更紧凑地引用对象或属性。使用 this。with() 实际上是一个高阶函数,您可以在 lambda 中指定如何处理提供的对象。
- 使用
with()将fishExamples()中的鱼名首字母大写。在花括号内,this指的是传递给with()的对象。
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
with (fish.name) {
this.capitalize()
}
}- 没有输出,因此请在周围添加
println()。this是隐式的,不需要,因此您可以将其移除。
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
with (fish.name) {
println(capitalize())
}
}⇒ Splashy
第 3 步:创建高阶函数
在底层,with() 是一个高阶函数。如需了解其运作方式,您可以自行制作一个仅适用于字符串的 with() 大大简化版本。
- 在
Fish.kt中,定义一个接受两个实参的函数myWith()。实参是要操作的对象,以及定义操作的函数。函数的实参名称惯例为block。在这种情况下,该函数不返回任何内容,这由Unit指定。
fun myWith(name: String, block: String.() -> Unit) {}在 myWith() 中,block() 现在是 String 的扩展函数。被扩展的类通常称为接收器对象。因此,在本例中,name 是接收器对象。
- 在
myWith()的正文中,将传入的函数block()应用于接收器对象name。
fun myWith(name: String, block: String.() -> Unit) {
name.block()
}- 在
fishExamples()中,将with()替换为myWith()。
fun fishExamples() {
val fish = Fish("splashy") // all lowercase
myWith (fish.name) {
println(capitalize())
}
}fish.name 是名称实参,println(capitalize()) 是代码块函数。
- 运行该程序,它会像以前一样运行。
⇒ Splashy
第 4 步:探索更多内置扩展服务
with() 扩展 lambda 非常有用,是 Kotlin 标准库的一部分。以下是一些您可能会觉得有用的其他功能:run()、apply() 和 let()。
run() 函数是一种可与所有类型搭配使用的扩展函数。它接受一个 lambda 作为实参,并返回执行该 lambda 的结果。
- 在
fishExamples()中,对fish调用run()以获取名称。
fish.run {
name
}此方法仅返回 name 属性。您可以将该值赋给变量或将其输出。这实际上并不是一个有用的示例,因为您只需访问该属性即可,但 run() 对于更复杂的表达式可能很有用。
apply() 函数与 run() 类似,但它会返回应用到的已更改对象,而不是 lambda 的结果。这对于对新创建的对象调用方法非常有用。
- 复制
fish并调用apply()以设置新副本的名称。
val fish2 = Fish(name = "splashy").apply {
name = "sharky"
}
println(fish2.name)
⇒ sharky
let() 函数与 apply() 类似,但它会返回经过更改的对象副本。这对于将多个操作串联在一起非常有用。
- 使用
let()获取fish的名称,将其大写,将另一个字符串与其连接,获取该结果的长度,将该长度加 31,然后打印结果。
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})⇒ 42
在此示例中,it 所指的对象类型依次为 Fish、String、String 和 Int。
- 在调用
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 作为参数的函数。试试下面的示例。
- 在 example 中,创建一个 Java 类
JavaRun,并将以下内容粘贴到该文件中。
package example;
public class JavaRun {
public static void runNow(Runnable runnable) {
runnable.run();
}
}在 Kotlin 中,您可以在类型前面加上 object:,以实例化实现接口的对象。此方法可用于向 SAM 传递参数。
- 返回到
Fish.kt,创建一个函数runExample(),该函数使用object:创建Runnable。该对象应通过打印"I'm a Runnable"来实现run()。
fun runExample() {
val runnable = object: Runnable {
override fun run() {
println("I'm a Runnable")
}
}
}- 使用您创建的对象调用
JavaRun.runNow()。
fun runExample() {
val runnable = object: Runnable {
override fun run() {
println("I'm a Runnable")
}
}
JavaRun.runNow(runnable)
}- 从
main()调用runExample()并运行程序。
⇒ I'm a Runnable
虽然打印内容需要做很多工作,但这是 SAM 工作方式的一个很好的示例。当然,Kotlin 提供了一种更简单的方法来实现此目的,即使用 lambda 代替对象,从而使此代码更加紧凑。
- 移除
runExample中的现有代码,将其更改为使用 lambda 调用runNow(),然后运行该程序。
fun runExample() {
JavaRun.runNow({
println("Passing a lambda as a Runnable")
})
}
⇒ Passing a lambda as a Runnable
- 您可以使用最后一个参数的调用语法,使代码更加简洁,并摆脱圆括号。
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 训练营:欢迎来到本课程”。