Kotlin-标准库秘籍-全-

Kotlin 标准库秘籍(全)

原文:zh.annas-archive.org/md5/e231444d5fdb5db5bf5bbb1f0dd473c7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《Kotlin 标准库食谱》的主要目的是以友好的方式帮助您尽可能快地深入了解高级语言概念和特性。它涵盖了各种难度级别的广泛通用编程问题,包括设计模式、函数式编程、数据处理等。本书由一系列食谱组成,每个食谱都提出一个特定问题,并逐步解释如何有效地解决它。书中对标准库的所有展示特性都进行了详细解释,让您能够轻松发现它们。

本书将帮助软件开发人员轻松地将 Kotlin 集成到现有的 JVM 和 JavaScript 项目中。书中包含的示例可以轻松地应用于您的项目。如果您更喜欢在您喜欢的 IDE 中跟踪和测试食谱,您也可以从本书的 GitHub 仓库中获取现成的解决方案。完成本书后,您应该对语言的先进概念有专家知识和洞察力,从而能够有效地解决日常编程挑战。

本书面向的对象

本书非常适合那些已经熟悉 Kotlin 基础知识,并希望发现如何使用最先进的解决方案有效地解决日常编程问题的人。愿意从其他语言(尤其是 Java、Scala 和 JavaScript)切换到 Kotlin 的资深程序员也会发现本书很有帮助。

本书涵盖的内容

第一章, 范围、数列和序列,介绍了 Kotlin 的范围和序列的概念。它展示了如何通过定义自定义序列来处理常见的算法问题,以及如何为自定义类定义范围。

第二章, 表达性函数和可调整接口,展示了如何利用语言的内置特性来设计函数和接口。本章解释了如何实现干净、可重用的函数和包含默认实现的可扩展接口。食谱还涵盖了语言的其他特性,如内联闭包、解构变量、具现化类型参数以及其他有助于设计更灵活和自然代码的有用技巧。

第三章, 使用 Kotlin 函数式编程特性塑造代码,展示了如何通过采用最先进的函数式编程模式来解决现实生活中的编程挑战。本章帮助读者熟悉 Kotlin 标准库提供的函数式编程概念以及内置语言特性。

第四章,强大的数据处理,专注于展示标准库对集合上的声明式操作的支持。包含的食谱提供了解决与数据集转换、减少或过滤相关的各种编程问题的解决方案。本章展示了如何通过使用标准库中内置的强大功能,以函数式编程风格实践数据操作。

第五章,采用 Kotlin 概念的优雅设计模式,介绍了针对 Kotlin 实现流行设计模式的特定方法,包括观察者、懒代理、Builder、Strategy 等。下一章中介绍的设计模式都基于现实生活中的例子,强调了委托模式的好处。

第六章,友好的 I/O 操作,介绍了标准库中可用的有用扩展函数,这些函数简化了 I/O 操作的工作。本章重点介绍了文件上的读写操作的常见用例、与流和缓冲区一起工作,以及 Kotlin 在特定目录中遍历文件的方法。

第七章,让异步编程再次伟大,是关于异步编程的深入指南,重点关注 Kotlin 协程框架及其在现实生活中的应用。本章展示了如何通过在后台以非阻塞方式执行代码的部分来优化和改进之前的例子。在这里,你还可以找到一个使用 Retrofit 库和协程框架实现异步 REST 客户端的实用示例。

第八章,Android、JUnit 和 JVM UI 框架的最佳实践,涵盖了针对流行框架的特定实际问题。总的来说,它将重点关注 Android 平台特定的方面和异步 UI 编程,包括 Android 和 JVM 框架(如 JavaFX 和 Swing)中的协程。它还将帮助你使用 JUnit 框架为 JVM 平台编写有效的单元测试。与单元测试相关的食谱还将包括更高级的主题,例如使用 Mockito Kotlin 库模拟依赖关系,以及基于协程框架测试异步代码。

第九章,杂项,提供了 Kotlin 开发者日常遇到的各种问题和问题的实用解决方案。

为了充分利用这本书

为了有效地从本书中学习,你应该按步骤遵循包含的食谱,并尝试自己实现解决方案。您可以从本书的 GitHub 仓库github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook下载示例代码,并将其导入 IntelliJ IDEA 和 Android Studio。如果您有任何问题,您可以在 IDE 中立即运行并测试每个食谱。

下载示例代码文件

您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com上登录或注册。

  2. 选择“支持”标签。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入本书的名称,并遵循屏幕上的说明。

下载文件后,请确保使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

书籍的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的图书和视频目录的代码包可供在github.com/PacktPublishing/上使用。查看它们吧!

使用的约定

本书使用了多种文本约定。

CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“我们可以使用为IntProgressionLongProgressionCharProgression类型提供的扩展函数来实现这一点,该函数称为reversed()。”

代码块如下设置:

val daysOfYear: IntRange = 1..365
for(day in daysOfYear.reversed()) {
    println("Remaining days: $day")
}

当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:

val sequence = sequenceOf("a", "b", "c", "d", "e", "f", "g", "h")
val transformedSequence = sequence.map {
    println("Applying map function for $it")
 it
}

任何命令行输入或输出都应如下编写:

[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

粗体: 表示新术语、重要单词或屏幕上看到的单词。

警告或重要提示如下所示。

小贴士和技巧如下所示。

部分

在本书中,您将找到一些频繁出现的标题(准备工作如何操作…如何工作…还有更多…另请参阅)。

为了清楚地说明如何完成一个食谱,请按照以下方式使用这些部分:

准备工作

本节将告诉您在食谱中可以期待什么,并描述如何设置任何软件或任何为食谱所需的初步设置。

如何操作…

本节包含遵循食谱所需的步骤。

它是如何工作的...

本节通常包含对前一个章节发生情况的详细解释。

更多内容...

本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。

参见

本节提供了指向其他有用信息的链接,这些信息对食谱有帮助。

联系我们

我们欢迎读者的反馈。

一般反馈:请发送邮件至 feedback@packtpub.com,并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请通过 questions@packtpub.com 发送邮件给我们。

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将非常感激您能向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何非法副本,我们将非常感激您能提供位置地址或网站名称。请通过 copyright@packtpub.com 联系我们,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

评论

留下评论。一旦您阅读并使用过本书,为何不在购买该书的网站上留下评论?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需更多关于 Packt 的信息,请访问 packtpub.com

第一章:范围、进度和序列

在本章中,我们将介绍以下食谱:

  • 探索使用范围表达式遍历字母字符

  • 使用自定义步长值的进度遍历范围

  • 构建自定义进度来遍历日期

  • 使用范围表达式与流程控制语句

  • 发现序列的概念

  • 将序列应用于解决算法问题

简介

本章将重点解释范围表达式序列的优势。Kotlin 标准库提供的这些强大的数据结构概念可以帮助您提高代码的质量和可读性,以及其安全性和性能。范围表达式提供了一种声明性的方式,通过for循环迭代可比较类型的集合。它们对于实现简洁和安全的控制流语句和条件也非常有用。Sequence类作为Collection类型的补充,提供了其元素的内置惰性求值。在许多情况下,使用序列可以帮助优化数据处理操作,并在计算复杂度和内存消耗方面使代码更加高效。本章涵盖的食谱将专注于解决现实生活中的编程问题。此外,同时,它们还将解释这些概念在底层是如何工作的。

探索使用范围表达式遍历字母字符

Kotlin 标准库提供的范围是一个强大的解决方案,用于以自然和安全的方式实现迭代和条件语句。范围可以被理解为一种抽象数据类型,它表示一组可迭代的元素,并允许以声明性的方式遍历它们。来自kotlin.ranges包的ClosedRange接口是范围数据结构的基本模型。它包含对范围首尾元素的引用,并提供contains(value: T): BooleanisEmpty(): Boolean函数,这些函数负责检查指定元素是否属于范围以及范围是否为空。在本食谱中,我们将学习如何声明一个由字母字符组成的范围,并以降序遍历它。

准备工作

Kotlin 标准库提供了允许声明整数、原始类型(如IntLongChar)范围的函数。要定义一个新的范围实例,我们可以使用rangeTo()函数。例如,我们可以以下这种方式声明一个从01000的整数范围:

val range: IntRange = 0.rangeTo(1000)

rangeTo()函数也有其自己的特殊运算符等价物,即..,它允许使用更自然的语法声明一个范围:

val range: IntRange = 0..1000

此外,为了以降序声明一系列元素,我们可以使用downTo()函数。

如何做...

  1. 声明字母字符的降序范围:
'Z' downTo 'A'
  1. 创建一个for循环来遍历范围:
for (letter in 'Z' downTo 'A') print(letter)

它是如何工作的...

因此,我们将得到以下代码打印到控制台:

ZYXWVUTSRQPONMLKJIHGFEDCBA

如你所见,还有downTo()扩展函数变体用于Char类型。我们使用它来创建从ZA的字符范围。请注意,多亏了内联表示法,我们可以在调用函数时省略括号——'Z' downTo 'A'

接下来,我们创建了一个for循环,它遍历范围并打印出后续的Char元素。使用in运算符,我们指定了循环中正在迭代的对象——就是这样!正如你所见,Kotlin 中for循环的语法整洁且易于使用。

原始类型范围实现,如IntRangeLongRangeCharRange,在底层也包含Iterator接口实现。在底层使用for循环遍历范围时,它们被使用。实际上,实现Iterable接口的范围被称为进度。在底层,IntRangeLongRangeCharRange类从IntProgressionLongProgressionCharProgression基类继承,并内部提供Iterator接口的实现。

更多内容...

还有一个方便的方法可以反转已定义进度的顺序。我们可以使用为IntProgressionLongProgressionCharProgression类型提供的扩展函数reversed()来实现这一点。它返回具有元素顺序反转的新进度实例。以下是如何使用reversed()函数的示例:

val daysOfYear: IntRange = 1..365
for(day in daysOfYear.reversed()) {
    println("Remaining days: $day")
}

前面的for循环将以下文本打印到控制台:

Remaining days: 365
Remaining days: 364
Remaining days: 363
…
Remaining days: 2
Remaining days: 1

Kotlin 标准库还提供了一个名为until()的便捷扩展函数,它允许声明不包含最后一个元素的范围。当与包含内部集合且不提供优雅接口访问它们的类一起工作时,这非常有用。一个很好的例子是 Android 的ViewGroup类,它是一个用于子View类型对象的容器。以下示例展示了如何遍历任何给定ViewGroup实例子元素的下一个索引,以修改每个子元素的状态:

val container: ViewGroup = activity.findViewById(R.id.container) as ViewGroup
(0 until container.childCount).forEach {
    val child: View = container.getChildAt(it)
    child.visibility = View.INVISIBLE
}

until()内联函数有助于使循环条件清晰且易于理解。

参见

  • 这个配方让我们了解了 Kotlin 标准库对原始类型范围实现的易用性。如果我们想使用for循环遍历非原始类型,可能会出现一个问题。然而,我们很容易为任何Comparable类型声明一个范围。这将在构建自定义进度以遍历日期配方中展示。

  • 正如你所注意到的,我们使用in运算符来指定循环中正在迭代的对象。然而,还有其他场景中,in!in运算符可以与范围一起使用。我们将在使用范围表达式与流程控制语句菜谱中深入探讨它们。

使用自定义步长值遍历范围

除了对Iterator实例这样做之外,对于整型类型(如IntLongChar类型)的进度实现也包括step属性。step值指定了范围后续元素之间的间隔。默认情况下,进度的step值等于1。在本例中,我们将学习如何使用step值等于2遍历字母字符的范围。在结果中,我们希望每第二个字母字符被打印到控制台。

准备工作

Kotlin 标准库提供了一个方便的方法来创建具有自定义step值的进度。我们可以使用名为step()的进度整型类型的扩展函数来实现这一点。我们还可以利用中缀表示法,如下声明具有自定义step的进度:

val progression: IntProgression = 0..1000 step 100

如果我们在for循环中使用progression,它将迭代 10 次:

val progression: IntProgression = 0..1000 step 100
for (i in progression) {
    println(i)
}

我们也可以通过以下方式使用while循环达到相同的结果:

var i = 0
while (i <= 1000) {
    println(i)
    i += 100
}

如何操作...

  1. 使用downTo()函数声明一个Char类型的范围:
'z' downTo 'a'
  1. 使用step()函数将范围转换为具有自定义step值的进度:
'z' downTo 'a' step 2
  1. 使用forEach()函数遍历进度的元素,并将每个元素打印到控制台:
('z' downTo 'a' step 2).forEach { character -> print(character) }

它是如何工作的...

在结果中,我们将得到以下代码打印到控制台:

zxvtrpnljhfdb

在开始时,我们使用downTo()函数声明了一个包含所有字母字符的降序范围的变量。然后,我们使用step()函数将范围转换为包含每个第二个字符的自定义进度。最后,我们使用Iterable.forEach()函数遍历进度的下一个元素,并将每个元素打印到控制台。

step()扩展函数适用于IntProgressionLongProgressionCharProgression类型。在内部,它创建一个进度的新实例,复制原始进度的属性,并设置新的step值。

参见

  • 除了迭代之外,范围表达式对于定义流程控制条件也很有用。你可以在使用范围表达式与流程控制语句菜谱中了解更多信息。

构建用于遍历日期的自定义进度

Kotlin 为原始类型的范围提供了内置支持。在之前的菜谱中,我们使用了IntRangeCharRange类型,这些类型包含在 Kotlin 标准库中。然而,通过实现Comparable接口,我们可以为任何类型实现自定义的递增。在这个菜谱中,我们将学习如何创建LocalDate类型的递增,并了解如何轻松地遍历日期。

准备工作

为了完成任务,我们首先需要熟悉ClosedRangeIterator接口。我们需要使用它们来为LocalDate类声明一个自定义的递增:

public interface ClosedRange<T: Comparable<T>> {
    public val start: T
    public val endInclusive: T
    public operator fun contains(value: T): Boolean {
        return value >= start && value <= endInclusive
    }                                      
    public fun isEmpty(): Boolean = start > endInclusive
}

Iterator接口提供了关于后续值及其可用性的信息:

public interface Iterator<out T> {
    public operator fun next(): T
    public operator fun hasNext(): Boolean
}

ClosedRange接口提供了范围的最低值和最高值。它还提供了contains(value: T): BooleanisEmpty(): Boolean函数,分别检查给定值是否属于范围以及范围是否为空。这两个函数在ClosedRange接口中提供了默认实现。因此,我们不需要在我们的自定义ClosedRange接口实现中重写它们。

如何实现...

  1. 让我们从为LocalDate类型实现Iterator接口开始。我们将创建一个自定义的LocalDateIterator类,该类将实现Iterator<LocalDate>接口:
class DateIterator(startDate: LocalDate,
                   val endDateInclusive: LocalDate,
                   val stepDays: Long) : Iterator<LocalDate> {
    private var currentDate = startDate
    override fun hasNext() = currentDate <= endDateInclusive
    override fun next(): LocalDate {
        val next = currentDate
        currentDate = currentDate.plusDays(stepDays)
        return next
    }
}
  1. 现在,我们可以实现LocalDate类型的递增。让我们创建一个新的类,称为DateProgression,它将实现Iterable<LocalDate>ClosedRange<LocalDate>接口:
class DateProgression(override val start: LocalDate,
                      override val endInclusive: LocalDate,
                      val stepDays: Long = 1) : 
                                                Iterable<LocalDate>, 
                                                ClosedRange<LocalDate> {
    override fun iterator(): Iterator<LocalDate> {
        return DateIterator(start, endInclusive, stepDays)
    } 

    infix fun step(days: Long) = DateProgression(start, endInclusive, days)
}
  1. 最后,为LocalDate类声明一个自定义的rangeTo操作符:
operator fun LocalDate.rangeTo(other: LocalDate) = DateProgression(this, other)

工作原理...

现在,我们能够为LocalDate类型声明范围表达式。让我们看看如何使用我们的实现。在下面的示例中,我们将使用我们自定义的LocalDate.rangeTo操作符实现来创建一个日期范围并迭代其元素:

val startDate = LocalDate.of(2020, 1, 1)
val endDate = LocalDate.of(2020, 12, 31)
for (date in startDate..endDate step 7) {
    println("${date.dayOfWeek} $date ")
}

因此,我们将以一周为间隔将日期打印到控制台:

WEDNESDAY 2020-01-01
WEDNESDAY 2020-01-08
WEDNESDAY 2020-01-15
WEDNESDAY 2020-01-22
WEDNESDAY 2020-01-29
WEDNESDAY 2020-02-05
...
WEDNESDAY 2020-12-16
WEDNESDAY 2020-12-23
WEDNESDAY 2020-12-30

DateIterator类包含三个属性——currentDate: LocalDateendDateInclusive: LocalDatestepDays: Long。一开始,currentDate属性使用构造函数中传入的startDate值初始化。在next()函数内部,我们返回currentDate值,并使用给定的stepDays属性间隔将其更新到下一个日期值。

DateProgression 类结合了 Iterable<LocalDate>ClosedRange<LocalDate> 接口的特性。它通过返回 DateIterator 实例来提供 Iterable 接口所需的 Iterator 对象。它还重写了 ClosedRange 接口的 startendInclusive 属性。还有一个默认值为 1stepDays 属性。请注意,每次调用 step 函数时,都会创建一个新的 DateProgression 类实例。

您可以使用相同的模式为任何实现 Comparable 接口的类实现自定义序列。

使用范围表达式与流程控制语句

除了迭代之外,当涉及到使用流程控制语句时,Kotlin 范围表达式可能非常有用。在本食谱中,我们将学习如何将范围表达式与 ifwhen 语句一起使用,以优化代码并使其更安全。在本食谱中,我们将考虑使用 in 操作符来定义 if 语句条件的示例。

准备工作

Kotlin 范围表达式——由 ClosedRange 接口表示——实现了一个 contains(value: T): Boolean 函数,该函数返回一个信息,表明给定的参数是否属于该范围。这个特性使得将范围与控制流指令一起使用变得方便。contains() 函数也有其等价的操作符 in 和其否定 !in

如何操作...

  1. 让我们创建一个变量,并给它分配一个随机整数值:
val randomInt = Random().nextInt()
  1. 现在,我们可以使用范围表达式检查 randomInt 值是否属于从 010(包括 10)的整数范围:
if (randomInt in 0..10) {
    print("$randomInt belongs to <0, 10> range")
} else {
    print("$randomInt doesn't belong to <0, 10> range")
}

工作原理...

我们已经使用范围表达式与 in 操作符一起定义了 if 语句的条件。条件语句易于阅读且简洁。相比之下,等效的经典实现可能看起来像这样:

val randomInt = Random(20).nextInt()
if (randomInt >= 0 && randomInt <= 10) {
    print("$randomInt belongs to <0, 10> range")
} else {
    print("$randomInt doesn't belong to <0, 10> range")
}

毫无疑问,使用范围和 in 操作符的声明式方法比经典的命令式条件语句更简洁、更容易阅读。

更多内容...

范围表达式还可以增强 when 表达式的使用。在下面的示例中,我们将实现一个简单的函数,该函数将负责将学生的考试成绩映射到相应的等级。假设我们有以下用于学生等级的枚举类模型:

enum class Grade { A, B, C, D }

我们可以定义一个函数,将考试分数值(在 0100 % 范围内)映射到适当的等级(ABCD),使用 when 表达式,如下所示:

fun computeGrade(score: Int): Grade =
        when (score) {
            in 90..100 -> Grade.A
            in 75 until 90 -> Grade.B
            in 60 until 75 -> Grade.C
            in 0 until 60 -> Grade.D
            else -> throw IllegalStateException("Wrong score value!")
        }

使用范围与 in 操作符一起使用,使得 computeGrade() 函数的实现比使用传统的比较操作符(如 <><=>=)的经典等效实现更简洁、更自然。

相关内容

  • 如果你想了解更多关于 lambda 表达式、内联表示法和运算符重载的信息,请继续阅读第二章,表达性函数和可调整接口

发现序列的概念

在高级功能方面,SequenceCollection数据结构几乎相同。它们都允许遍历它们的元素。Kotlin 标准库中还有许多强大的扩展函数,为每个数据结构提供声明式数据处理的操作。然而,Sequence数据结构在底层的行为不同——它延迟对其元素的任何操作,直到它们最终被消费。它在遍历它们的同时实例化后续元素。Sequence的这些特性,称为延迟评估,使这种数据结构与 Java 概念Stream非常相似。为了更好地理解这一切,我们将实现一个简单的数据处理场景来分析Sequence的效率和行为,并将我们的发现与基于Collection的实现进行对比。

准备工作

让我们考虑以下示例:

val collection = listOf("a", "b", "c", "d", "e", "f", "g", "h")
val transformedCollection = collection.map {
    println("Applying map function for $it")
    it
}
println(transformedCollection.take(2))

在第一行,我们创建了一个字符串列表并将其分配给collection变量。接下来,我们正在将map()函数应用于列表。映射操作允许我们转换集合中的每个元素,并返回一个新值而不是原始值。在我们的例子中,我们只是用它来观察是否调用了map(),通过向控制台打印消息。最后,我们想要使用take()函数过滤我们的集合,只包含前两个元素,并将列表的内容打印到控制台。

最后,前面的代码将打印以下输出:

Applying map function for a
Applying map function for b
Applying map function for c
Applying map function for d
Applying map function for e
Applying map function for f
Applying map function for g
Applying map function for h
[a, b]

如您所见,map()函数已正确应用于集合的每个元素,而take()函数已正确过滤了列表中的元素。然而,如果我们处理的是更大的数据集,这并不是一个最优的实现。我们最好等到我们知道数据集的哪些特定元素是我们真正需要的,然后再对这些元素应用这些操作。结果证明,我们可以很容易地使用Sequence数据结构来优化我们的场景。让我们在下一节中探讨如何实现它。

如何实现...

  1. 为给定元素声明一个Sequence实例:
val sequence = sequenceOf("a", "b", "c", "d", "e", "f", "g", "h")
  1. 将映射操作应用于序列的元素:
val sequence = sequenceOf("a", "b", "c", "d", "e", "f", "g", "h")
val transformedSequence = sequence.map {
    println("Applying map function for $it")
 it
} 
  1. 将序列的前两个元素打印到控制台:
val sequence = sequenceOf("a", "b", "c", "d", "e", "f", "g", "h")

val transformedSequence = sequence.map {
    println("Applying map function for $it")
    it
}
println(transformedSequence.take(2).toList())

它是如何工作的...

基于序列(Sequence)的实现将给出以下输出:

Applying map function for a
Applying map function for b
[a, b]

如您所见,用Sequence类型替换Collection数据结构使我们能够获得所需的优化。

在本食谱中考虑的场景被实现得完全相同——首先使用List,然后使用Sequence类型。然而,我们可以注意到Sequence数据结构与Collection的行为差异。map()函数仅应用于序列的前两个元素,尽管在映射转换声明之后调用了take()函数。还值得注意的是,在使用Collection的示例中,当调用map()函数时,映射是立即执行的。在Sequence的情况下,映射是在将元素打印到控制台时进行的,更确切地说,是在将Sequence转换为以下代码行中的List类型时进行的:

println(transformedSequence.take(2).toList())

更多...

Collection转换为Sequence有一种方便的方法。我们可以使用 Kotlin 标准库为Iterable类型提供的asSequence()扩展函数来完成此操作。要将Sequence实例转换为Collection实例,您可以使用toList()函数。

参见

  • 多亏了Sequence的延迟求值特性,我们避免了不必要的计算,同时提高了代码的性能。延迟求值允许实现具有无限元素数量的序列,并且在实现算法时也表现出有效性。

  • 你可以在将序列应用于解决算法问题食谱中探索基于Sequence的斐波那契算法的实现。它更详细地介绍了另一个用于定义序列的有用函数,称为generateSequence()

将序列应用于解决算法问题

在本食谱中,我们将熟悉generateSequence()函数,该函数提供了一种轻松定义各种类型序列的方法。我们将使用它来实现生成斐波那契数的算法。

准备工作

generateSequence()函数的基本变体声明如下:

fun <T : Any> generateSequence(nextFunction: () -> T?): Sequence<T>

它接受一个名为nextFunction的参数,该参数是一个返回序列下一个元素的函数。在底层,它通过Sequence类内部实现中的Iterator.next()函数被调用,并允许在消耗序列值的同时实例化下一个要返回的对象。

在以下示例中,我们将实现一个有限序列,该序列从100发出整数:

var counter = 10
val sequence: Sequence<Int> = generateSequence {
    counter--.takeIf { value: Int -> value >= 0 }
}
print(sequence.toList())

应用到当前counter值的takeIf()函数检查其值是否大于或等于0。如果条件得到满足,它返回counter值;否则,它返回null。每当generateSequence()函数返回null时,序列停止。在takeIf函数返回值后,counter值将进行后递减。前面的代码将导致以下数字被打印到控制台:

[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

斐波那契数列的后续值是通过将它们的两个前一个值相加生成的。此外,前两个值等于 01。为了实现这样一个序列,我们将使用一个带有额外 seed 参数的 generateSequence() 函数的扩展版本,声明如下:

fun <T : Any> generateSequence(seed: T?, nextFunction: (T) -> T?): Sequence<T>

如何做到这一点...

  1. 声明一个名为 fibonacci() 的函数,并使用 generateSequence() 函数定义序列的下一个元素的公式:
fun fibonacci(): Sequence<Int> {
    return generateSequence(Pair(0, 1)) { Pair(it.second, it.first + it.second) }
            .map { it.first }
}
  1. 使用 fibonacci() 函数将下一个斐波那契数打印到控制台:
println(fibonacci().take(20).toList())

它是如何工作的...

因此,我们将得到下一个 20 个斐波那契数打印到控制台:

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]

generateSequence() 中的额外 seed 参数提供了一个起始值。在计算第二个值时,将 nextFunction() 函数应用于 seed。随后,它使用前一个值生成每个后续元素。然而,在斐波那契数列的情况下,我们有两个初始值,我们需要一对前一个值来计算下一个值。因此,我们将它们包装在 Pair 类型实例中。基本上,我们正在定义一个 Pair<Int, Int> 类型元素的序列,并在每次 nextFunction() 调用中返回一个新的对,它包含相应更新的值。最后,我们只需要使用 map() 函数将每个 Pair 元素替换为其 first 属性的值。因此,我们得到一个无限序列,返回后续的斐波那契数。

第二章:表达式函数和可调整接口

在本章中,我们将涵盖以下菜谱:

  • 声明具有默认参数的可调整函数

  • 声明包含默认实现的接口

  • 扩展类的功能

  • 解构类型

  • 返回多个数据

  • 内联闭包类型的参数

  • 函数的内联表示法

  • 使用泛型重载参数的智能类型检查

  • 重载运算符

简介

本章将重点探讨一些 Kotlin 特性,这些特性可以帮助编写健壮、灵活且干净的函数和接口。阅读以下菜谱后,您将了解语言特定的支持和方法,用于减少样板代码和运行时性能改进。您还将了解标准库函数在底层是如何实现的,以及如何有效地与它们一起工作。

声明具有默认参数的可调整函数

当创建新函数时,我们经常需要允许一些参数是可选的。这迫使我们使用方法重载来创建具有相同名称但与不同用例和场景相关的不同参数集的多个函数声明。通常,在底层,每个函数变体都会调用具有默认实现的基本函数。让我们考虑一个简单的函数示例,该函数计算以恒定加速度率移动的物体的位移:

fun calculateDisplacement(initialSpeed: Float, 
                          acceleration: Float, 
                          duration: Long): Double =
    initialSpeed * duration + 0.5 * acceleration * duration * duration

我们可能还需要为对象初始速度始终等于零的情况提供一个位移计算。在这种情况下,我们最终会以以下方式对基本函数进行重载:

fun calculateDisplacement(acceleration: Float, duration: Long): Double = calculateDisplacement(0f, acceleration, duration)

然而,Kotlin 允许您通过单个具有可选参数的函数来减少多个声明,并处理多种不同的用例。在本菜谱中,我们将设计一个具有可选 initialSpeed: Float 参数的可调整版本的 calculateDisplacement() 函数。

如何做到这一点...

  1. 让我们为该函数声明基本实现:
fun calculateDisplacement(initialSpeed: Float, 
                          acceleration: Float, 
                          duration: Long): Double =
    initialSpeed * duration + 0.5 * acceleration * duration * 
    duration
  1. 让我们为 initialSpeed 参数声明一个默认值:*
fun calculateDisplacement(initialSpeed: Float = 0f, 
                          acceleration: Float, 
                          duration: Long): Double =
    initialSpeed * duration + 0.5 * acceleration * duration * 
    duration

它是如何工作的...

我们已经为 initialSpeed 参数声明了一个默认值,等于 0。一旦我们分配了默认值,initialSpeed 参数就变成了可选的。现在,我们可以在调用函数时省略它,如下面的示例所示:

val displacement = calculateDisplacement(acceleration = 9.81f, duration = 1000)

注意,如果我们省略了一些参数并使用它们的默认值,我们必须明确指定其他参数的值及其名称。这允许编译器将值映射到特定的参数。当然,我们能够使用标准方式覆盖默认值:

val displacement = calculateDisplacement(10f, 9.81f, 1000)

参见

  • Kotlin 使得声明包含默认函数实现的接口成为可能。您可以在 声明包含默认实现的接口 菜谱中了解更多关于此功能的信息。

声明包含默认实现的接口

Kotlin 通过提供为其函数声明默认实现和定义其属性默认值的能力,使接口成为一个强大的语言元素。这些功能将接口提升到了一个新的水平,允许你将其用于比简单合同声明更高级的应用程序。

在这个菜谱中,我们将定义一个可重用的接口,用于验证用户在抽象注册表单的输入字段中输入的电子邮件地址值。该接口将提供两个函数。第一个函数负责解析电子邮件地址并决定给定的值是否为有效的电子邮件地址,第二个函数负责从表单中输入的电子邮件文本中提取用户的登录名。

准备工作

声明具有默认函数实现的接口很简单。我们不需要声明函数头,还需要包括其体:

interface MyInterface {
    fun foo() {
        // default function body
    }
}

如何操作...

  1. 声明一个新的接口,称为 EmailValidator
interface EmailValidator {}
  1. 添加一个字符串属性,用于存储当前文本输入:
interface EmailValidator {
    var input: String
}
  1. isEmailValid() 函数添加到接口中:
interface EmailValidator {
    var input: String
    fun isEmailValid(): Boolean = input.contains("@")
}
  1. 添加 getUserLogin() 函数:
interface EmailValidator {
    var input: String

    fun isEmailValid(): Boolean = input.contains("@")

    fun getUserLogin(): String =
        if (isEmailValid()) {
            input.substringBefore("@")
        } else {
            ""
        }
}

它是如何工作的...

现在,让我们试一试,看看我们如何在实际操作中使用 EmailValidator 接口。假设我们有一个 RegistrationForm 类,它包含一个钩子方法,每次输入文本被修改时都会被调用:

class RegistrationForm() {
    fun onInputTextUpdated(text: String) {
        // do some actions on text changed
    }
}

为了使用我们的 EmailValidator 接口,我们需要声明一个实现该接口的类。我们可以修改 RegistrationForm 类以实现 EmailValidator 接口:

class RegistrationForm(override var input: String = ""): EmailValidator {
    fun onInputTextUpdated(newText: String) {
        this.input = newText

        if (!isEmailValid()) {
            print("Wait! You've entered wrong email...")
        } else {
            print("Email is correct, thanks: ${getUserLogin()}!")
        }
    }
}

每次调用 onInputUpdated() 函数时,我们都会更新在 EmailValidator 接口中声明的 input: String 属性。一旦更新完成,我们就使用 EmailValidator 接口的 isEmailValid()getUserLogin() 函数值。将函数实现提取到接口中使得它们可以被重用,并轻松地集成到多个类中。唯一需要实际实现的部分是 EmailValidator 接口的 input 属性,它存储用户插入的文本的当前状态。以平滑的方式集成 EmailValidator 接口使其在可重用性和适应不同场景的应用程序中表现出色。

还有更多...

重要的是要记住,尽管我们可以在接口中定义默认函数实现,但我们无法为接口属性实例化默认值。与类属性不同,接口属性是抽象的。它们没有可以存储当前值(状态)的后备字段。如果我们在一个接口中声明一个属性,我们需要在实现该接口的类或对象中实现它。这是接口和抽象类之间的主要区别。抽象类可以有构造函数,可以存储属性及其实现。

与 Java 一样,我们不能扩展多个类;然而,我们可以实现多个接口。当我们有一个类实现了包含默认实现的多个接口时,我们可能会遇到由具有相同签名的函数引起的冲突:

interface A {
    fun foo() {
        // some operations 
    }
}

interface B {
    fun foo() {
        // other operations
    }
}

在这种情况下,我们需要显式重写foo()函数以解决冲突:

class MyClass: A, B {
    override fun foo() {
        print("I'm the first one here!")
    }
}

否则,我们会得到以下错误:

Class 'MyClass' must override public open fun foo(): Unit because it inherits multiple interface methods of it.

参见

  • Kotlin 的一个类似特性是能够声明函数参数的默认值。您可以在使用默认参数声明可调整函数菜谱中了解更多信息。

扩展类的功能

在实现新功能或重构现有代码时,我们经常将代码的一部分提取到函数中,以便在不同的地方重用。如果提取的函数足够原子化,我们通常会将它导出到外部实用工具类中,这些类的首要目的是扩展现有类的功能。Kotlin 提供了一个有趣的替代方案。它提供了一个内置功能,允许我们使用扩展函数扩展属性来扩展其他类的功能。

在这个菜谱中,我们将扩展Array<T>类的功能,并向其添加一个swap(a:T, b: T)扩展函数,该函数负责交换数组中两个给定元素的位置。

准备工作

我们可以在项目的任何文件中声明扩展函数和扩展属性。然而,为了保持良好的组织结构,最好将它们放在专门的文件中。

扩展函数的语法与标准函数的语法非常相似。我们只需要添加有关新函数扩展的类型信息,如下所示:

fun SomeClass.newFunctionName(args): ReturnType {
    // body
}

如何做到这一点...

  1. 创建一个新文件,名为Extensions.kt,用于存储扩展函数的定义。

  2. 在其中实现swap()函数:

fun <T> Array<T>.swap(a: T, b: T) {
    val positionA = indexOf(a)
    val positionB = indexOf(b)

    if (positionA < 0 || positionB < 0) {
        throw IllegalArgumentException("Given param doesn't belong
        to the array")
    }

    val tmp = this[positionA]
    this[positionA] = this[positionB]
    this[positionB] = tmp
}

它是如何工作的...

因此,我们能够对Array类的任何实例调用swap函数。让我们考虑以下示例:

val array: Array<String> = arrayOf("a", "b", "c", "d")
array.swap("c", "b")
print(array.joinToString())

这将在控制台输出以下内容:

a, c, b, d

如您所见,我们可以在扩展函数中使用this关键字来访问类的当前实例。

还有更多...

除了扩展函数之外,Kotlin 还提供了扩展属性功能。例如,我们可以为List<T>类声明一个属性,该属性将保存有关最后一个元素索引值的详细信息:

val <T> List<T>.lastIndex: Int  get() = size - 1

扩展函数在 Kotlin 标准库的类中是一个广泛使用的模式。它们与 Java、Kotlin、JavaScript 以及项目内部和外部依赖中定义的本地类无缝工作。

解构类型

通常,将复杂类型的一个对象转换为多个变量是非常实用的。这允许你为变量提供适当的命名,并简化代码。Kotlin 提供了一个简单内置的功能来实现这一点,称为解构

data class User(val login: String, val email: String, val birthday: LocalDate)

fun getUser() = User("Agata", "ag@t.pl", LocalDate.of(1990, 1, 18))

val (name, mail, birthday) = getUser()

print("$name was born on $birthday")

因此,这段代码会在控制台打印以下信息:

Agata was born on 1990-01-18

非常棒!解构对于数据类是开箱即用的。Kotlin 标准库还为许多常见类型提供了这个功能。然而,当我们处理自定义的非数据类时,解构并不是明确可用的。特别是,当我们与其他语言(如 Java)编写的类库中的类一起工作时,我们需要手动定义解构机制。在这个菜谱中,我们将为以下定义的 Java 类实现解构:

// Java code
public class LightBulb {
    private final int id;
    private boolean turnedOn = false;

    public LightBulb(int id) {
        this.id = id;
    }

    public void setTurnedOn(boolean turnedOn) {
        this.turnedOn = turnedOn;
    }

    public boolean getTurnedOn() {
        return turnedOn;
    }

    public int getId() {
        return id;
    }
}

准备工作

Kotlin 中的解构声明是位置相关的,与在其他语言中可用的基于属性名称的声明相反。这意味着 Kotlin 编译器根据属性的顺序决定哪个类属性与哪个变量相关联。为了允许自定义类的解构,我们需要添加名为componentN的函数的实现,其中N代表带有operator关键字的标记的组件编号,以便在解构声明中使用它们。

如何实现...

  1. 声明一个扩展函数,返回LightBulb类的id属性:
operator fun LightBulb.component1() = this.id

  1. 添加另一个扩展componentN函数,用于返回turnedOn属性:
operator fun LightBulb.component2() = this.turnedOn

它是如何工作的...

一旦我们声明了适当的componentN函数,我们就可以从LightBulb类型对象的解构中受益:

val (id, turnedOn) = LightBulb(1)
print("Light bulb number $id is turned ${if (turnedOn) "on" else "off"}")

这段代码会在控制台打印以下输出:

Light bulb number 1 is turned off

如您所见,component1()函数被分配给解构声明的第一个变量——id。同样,第二个turnedOn变量被分配给component2()函数的结果。

还有更多...

由于解构对象赋值中的属性是位置相关的,有时我们被迫声明比我们想要使用的变量更多的变量。如果我们不需要使用某个值,我们可以使用下划线,以避免编译器提示未使用的变量,并稍微简化代码:

val (_, turnedOn) = LightBulb(1)
print("Light bulb is turned ${if (turnedOn) "on" else "off"}")

解构也适用于函数返回值:

val (login, domain) = "agata@magdalena.com".split("@")
print("login: $login, domain: $domain")

上述代码将返回以下输出:

login: agata, domain: magdalena.com

我们还可以在 lambda 表达式中使用解构声明:

listOf(LightBulb(0), LightBulb(1))
        .filter { (_, isOn) -> isOn }
        .map { (id, _) -> id }

解构声明的一个有用应用是迭代。例如,我们可以使用此功能遍历映射条目:

val lightBulbsWithNames = 
        mapOf(LightBulb(0) to "Bedroom", LightBulb(1) to "Kitchen")

for ((lightbulb, name) in lightBulbsWithNames) {
    lightbulb.turnedOn = true
}

返回多个数据

虽然 Kotlin 没有提供多返回功能,但由于数据类和解构声明,编写返回多个不同类型值的函数相当方便。在这个菜谱中,我们将实现一个返回两个数字除法结果的函数。结果将包含商和余数值。

如何操作...

  1. 让我们从声明一个用于返回类型的数据类开始:
data class DivisionResult(val quotient: Int, val remainder: Int)
  1. 让我们实现divide()函数:
fun divide(dividend: Int, divisor: Int): DivisionResult {
    val quotient = dividend.div(divisor)
    val remainder = dividend.rem(divisor)
    return DivisionResult(quotient, remainder)
}

它是如何工作的...

我们可以看到divide()函数的作用:

val dividend = 10
val divisor = 3
val (quotient, remainder) = divide(dividend, divisor)

print("$dividend / $divisor = $quotient r $remainder")

以下代码将打印以下输出:

10 / 3 = 3 r 1

由于我们返回的是数据类实例,即DivisionResult类,我们可以利用解构特性并将结果分配给一组单独的变量。

还有更多...

Kotlin 标准库提供了现成的PairTriple类。我们可以使用它们来返回任意类型的两个和三个值。这消除了为返回类型创建专用数据类的需求。另一方面,使用数据类使我们能够使用更有意义的名称,这增加了代码的清晰度。

以下示例演示了使用Pair类同时返回两个对象:

fun getBestScore(): Pair<String, Int> = Pair("Max", 1000)
val (name, score) = getBestScore()
print("User $name has the best score of $score points")

相关内容

  • 如果你想要更熟悉解构声明,可以查看解构类型菜谱。

内联闭包类型的参数

使用高阶函数可能会导致运行时性能下降。将函数作为 lambda 参数传递以及它们在函数体内的虚拟调用会导致运行时开销。然而,在许多情况下,我们可以通过内联 lambda 表达式参数来消除这种开销。

在这个菜谱中,我们将实现lock()函数,该函数将自动化与 Java java.util.concurrent.locks.Lock接口的工作。该函数将接受两个参数——Lock接口的一个实例以及在获取锁后应调用的函数。最后,我们的lock()函数应释放锁。我们还希望允许内联函数参数。

准备工作

要声明内联函数,我们只需在函数头部之前添加inline修饰符。

如何操作...

让我们声明一个带有两个参数的lock()函数——Lock接口的一个实例以及获取锁后要调用的函数:

inline fun performHavingLock(lock: Lock, task: () -> Unit) {
    lock.lock()
    try {
       task()
    }
    finally {
        lock.unlock()
    }
}

它是如何工作的...

performHavingLock()函数允许我们为其task参数提供的函数提供同步。

performHavingLock(ReentrantLock()) {
 print("Wait for it!")
}

因此,performHavingLock()函数将打印以下输出到控制台:

Wait for it!

在底层,inline修饰符会影响函数本身以及传递给它的 lambda 表达式。它们都将内联到生成的字节码中:

Lock lock = (Lock)(new ReentrantLock());
lock.lock();

try {
   String var2 = "Wait for it!";
   System.out.print(var2);
} finally {
   lock.unlock();
}

如果我们没有使用inline修饰符,编译器将创建一个Function0类型的单独实例,以便将 lambda 参数传递给performHavingLock()函数。内联 lambda 可能会导致生成的代码增长。然而,如果我们以合理的方式(即避免内联大型函数)进行内联,这将有助于性能。

还有更多...

如果你只想将函数传递的一些 lambda 表达式内联,你可以使用noinline修饰符标记一些函数参数:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {  
    // ... 
}

Kotlin 还允许声明内联类属性。inline修饰符可以与没有后置字段的属性的 getter 和 setter 方法一起使用。例如:

val foo: Foo  
    inline get() = Foo()  

var bar: Bar  
    get() = ...  
    inline set(v) { ... }

我们还可以注释整个属性:

inline var bar: Bar  
    get() = ...  
    set(v) { ... }

因此,内联的 getter 和 setter 将与常规的内联函数以相同的方式进行表示。

函数的中缀表示法

为了使我们的代码更接近自然语言,Kotlin 为包含单个参数的函数提供了中缀表示法。这样,我们可以不使用括号来调用函数。在本菜谱中,我们将学习如何为String类型设计一个名为concat()的中缀扩展函数,该函数负责连接两个字符串值。

准备中

为了启用函数的中缀表示法,我们只需在函数标题之前添加infix关键字。

如何做到...

声明concat()扩展函数并实现其主体:

infix fun String.concat(next: String): String = this + next

它是如何工作的...

让我们通过运行以下代码来测试concat()函数:

print("This" concat "is" concat "weird")

太棒了!我们刚刚将以下文本打印到控制台:

Thisisweird

还有更多...

Kotlin 标准库广泛使用中缀表示法。你可以利用中缀函数以整洁的方式塑造你的代码。一个值得注意的中缀函数是Map.Entry<K, V>类提供的to()扩展函数,它允许你以极简的方式声明映射条目:

val namesWithBirthdays: Map<String, LocalDate> =
        mapOf("Agata" to LocalDate.of(1990, 1, 18))

to()扩展函数是为泛型类型A和泛型参数类型B声明的,它返回一个Pair<A, B>类的实例。

标准库中提供了许多支持中缀表示法的其他函数。如果你检查你每天使用的函数的实现,可能会发现它们也有中缀形式。

参见

  • 你可以在重载运算符菜谱中了解另一个有助于使代码更易于阅读的酷特性。

使用泛型重载参数的智能类型检查

在实现支持泛型类型参数的函数时,我们经常需要提供有关对象类型在运行时的额外信息。在 JVM 平台上,类型在Class<T>类实例中有它们的表示。例如,当我们使用Gson库将 JSON 格式的数据解析到 Kotlin 类实例时,我们可能会遇到这样的需求:

data class ApiResponse(val gifsWithPandas: List<ByteArray>)
data class Error(val message: String)

fun parseJsonResponse(json: String): ApiResponse {
    Gson().fromJson(json, ApiResponse::class.java)
}

通常,由于 JVM 类型擦除,我们无法在运行时访问泛型类型参数。然而,Kotlin 允许你克服这一限制,因为它在运行时保留了类型参数。在这个示例中,我们将调整 Gson 的fromJson(json: String, Class<T>)函数,以消除额外的类型参数。

准备工作

确保你的项目中包含了 Gson 依赖项(github.com/google/gson)。如果你使用 Gradle,可以在构建脚本中使用以下声明来获取它:

dependencies {
    compile 'com.google.code.gson:gson:2.8.2'
}

为了在运行时使类型参数可访问,我们需要用reified修饰符标记它,并将函数标记为inline

如何实现...

  1. 创建一个新文件,我们将在这里放置扩展函数的实现(例如,GsonExtensions.kt

  2. 在文件内部,声明一个Gson类的扩展函数:

inline fun <reified T> Gson.fromJson(json: String): T { 
    return fromJson(json, T::class.java)
}

工作原理...

我们已经为Gson类型实现了一个扩展函数。由于添加了reified修饰符,我们可以在运行时访问泛型类型参数并将其传递给原始的fromGson()函数。

结果,我们能够在代码中使用更优雅版本的fromGson()函数:

data class ApiResponse(val gifsWithPandas: List<ByteArray>)

val response = Gson().fromJson<ApiResponse>(json)

我们还可以利用 Kotlin 智能转换并从函数调用中省略显式的类型声明:

val response: ApiResponse = Gson().fromJson(json)

重载运算符

Kotlin 语言提供了一套具有自己符号(例如,+-*/)和优先级的运算符。在编译时,Kotlin 编译器将它们转换为关联函数调用或更复杂的语句。我们还可以重载运算符并为其指定的类型声明自定义底层实现。此实现将应用于使用运算符的指定类型的实例。

在这个示例中,我们将定义一个名为Position的类,表示三维空间中点的当前坐标。然后,我们将为我们的类实现自定义的plusminus运算符,以便提供一种简单的方式来对其实例应用几何变换。结果,我们希望能够使用+-运算符符号更新由Position类表示的点的坐标。

准备工作

为了为特定类型重载运算符,我们需要提供一个具有与运算符对应的固定名称的成员函数或扩展函数。此外,重载运算符的函数需要用operator关键字标记。

在以下表中,您可以找到语言中可用的操作符分组及其对应的表达式,它们被转换成:

一元前缀

操作符 表达式
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

增量与减量

操作符 表达式
a++ a.inc()
a-- a.dec()

算术

操作符 表达式
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a..b a.rangeTo(b)

内联操作符

操作符 表达式
a in b b.contains(a)
a !in b !b.contains(a)

索引访问

操作符 表达式
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ..., i_n] a.get(i_1, ..., i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)

调用操作符

操作符 表达式
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ..., i_n) a.invoke(i_1, ..., i_n)

增强赋值

操作符 表达式
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)

等式和比较

操作符 表达式
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))
a > b a.comareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

如何做到这一点...

  1. 使用xyz属性声明与笛卡尔坐标系中当前位置相关的Position数据类:
data class Position(val x: Float, val y: Float, val z: Float)
  1. Position类添加一个plus操作符实现:
data class Position(val x: Float, val y: Float, val z: Float) {
    operator fun plus(other: Position) = 
      Position(x + other.x, y + other.y, z + other.z)
}
  1. 覆盖minus操作符:
data class Position(val x: Float, val y: Float, val z: Float) {
    operator fun plus(other: Position) = 
      Position(x + other.x, y + other.y, z + other.z)

    operator fun minus(other: Position) = 
      Position(x - other.x, y - other.y, z - other.z)
}

它是如何工作的...

现在我们可以使用Position类以及plusminus操作符。让我们尝试使用减号操作符:

val position1 = Position(132.5f, 4f, 3.43f)
val position2 = position1 - Position(1.5f, 400f, 11.56f)
print(position2)

就这样。前面的代码将打印以下结果到控制台:

Position(x=131.0, y=-396.0, z=-8.13)

还有更多...

一些操作符有它们对应的复合赋值操作符定义。一旦我们覆盖了plusminus操作符,我们就可以自动使用plusAssign (+=)minusAssign (-=)操作符。例如,我们可以使用plusAssign操作符来更新Position实例状态如下:

var position = Position(132.5f, 4f, 3.5f)
position += Position(1f, 1f, 1f)
print(position)

因此,我们将得到以下状态的position变量:

Position(x=133.5, y=5.0, z=4.5)

重要的是要注意,赋值操作符返回Unit.这使得它在更新实例时,在内存分配效率方面比原始基本操作符(例如,plusminus)是一个更好的选择。相比之下,基本操作符每次都会返回新的实例。

值得注意的是,Kotlin 还提供了对 Java 类的运算符重载。要重载运算符,我们只需向具有运算符名称和public可见性修饰符的类中添加一个适当的方法。以下是具有重载plus运算符的Position类的 Java 版本:

public class Position { 
        private final float x; 
        private final float y; 
        private final float z;

        public Position(float x, float y, float z) { 
            this.x = x; 
            this.y = y; 
            this.z = z;
        } 

        public int getX() { 
            return x; 
        } 

        public int getY() { 
            return y; 
        } 

        public float getZ() {
            return z;
        }

       public Position plus(Position pos) { 
 return new Position(pos.getX() + x, pos.getY() + y,
            pos.getZ() + z); 
 } 
}

在 Kotlin 代码中,可以这样使用它:

val position = Position(2.f, 9.f, 55.5f) += (2.f, 2.f, 2.f)

Kotlin 标准库还包含不同运算符的预定义实现。你应每天使用的运算符之一是MutableCollection类型的plus运算符。这允许以下方式向集合中添加新元素:

val list = mutableListOf("A", "B", "C")
list += "D"
print(list)

因此,前面的代码将在控制台打印以下输出:

[A, B, C, D]

第三章:使用 Kotlin 函数式编程特性塑造代码

在本章中,我们将涵盖以下菜谱:

  • 高效使用 lambda 表达式

  • 发现基本的范围函数——letalsoapply

  • 使用run范围函数以干净的方式初始化对象

  • 使用高阶函数

  • 函数柯里化

  • 函数组合

  • 实现 Either Monad 设计模式

  • 自动函数记忆化的方法

简介

尽管 Kotlin 被隐式地认可为一种面向对象的语言,但它仍然对其他编程风格和范式持开放态度。多亏了 Kotlin 的内置特性,我们能够轻松地将函数式编程模式应用到我们的代码中。能够从其他函数中返回函数或传递一个函数作为参数,使我们能够从延迟计算中受益。此外,我们能够在代码的不同层返回函数,而不是已经计算出的值。这导致了懒加载特性的出现。

与 Scala 或其他函数式编程语言相比,Kotlin 不需要我们使用专门的函数式风格设计模式。它也缺乏它们的一些现成实现。然而,作为回报,它在软件架构和实现细节方面为开发者提供了更多的灵活性。Kotlin 语言和标准库组件为基本的函数式编程概念提供了全面内置支持。更复杂的概念总是可以从头实现或从一些可用的外部库中重用。值得一试的项目有 Kotlin Arrow (arrow-kt.io) 和 funKTionale (github.com/MarioAriasC/funKTionale)。然而,请记住罗伯特·C·马丁的话——编写既面向对象又函数式的程序是完全可能的。这不仅可能,而且是可取的。不存在“OO vs FP”,两者是正交的,并且很好地共存 应该理解,函数式编程只是可用的工具之一。它应该被明智地使用,并且仅在适用的情况下使用。

本章重点介绍 Kotlin 内部支持的函数式编程特性。它通过使用最先进的函数式编程概念来解决实际问题,为您提供了实践经验。到本章结束时,您应该熟悉 Kotlin 语言对函数式编程方法的支持以及可以帮助实现它的标准库组件。

高效使用 lambda 表达式

在这个菜谱中,我们将探讨 lambda 和闭包的概念。我们将编写一部分 Android 应用程序代码,负责处理按钮点击操作。

准备工作

为了实现这个菜谱的代码,您需要使用 Android Studio IDE 创建一个新的 Android 应用程序项目。

假设我们有一个以下类,它是一种应用视图层的控制器:

class RegistrationScreen : Activity() {
    private val submitButton: Button by lazy { findViewById(R.id.submit_button) }  

    override fun onCreate(savedInstanceState: Bundle?) {
        // hook function called once the screen is displayed
    }
}

它包含对 submitButton: Button 实例的引用。在 onCreate() 函数中,我们将实现处理按钮点击的逻辑。一旦按钮被点击,我们希望使其不可见。

为了在按钮点击时执行某些操作,我们需要在 View 子类上调用 View.setOnClickListener(listener: OnClickListener) 函数。OnClickListener 是一个如下定义的函数式接口:

public interface OnClickListener {
    void onClick(View view);
}

在底层,当用户点击视图时,Android 操作系统会调用 onClick() 函数。

在 Kotlin 中实现函数式接口有两种方式:

  • 定义实现接口的对象:
val myInterfaceInstance = object: MyInterface {
    override fun foo() {
        // foo function body
    }
}
  • 将接口作为函数处理并实现它,例如,以 lambda 的形式:
val myInterfaceAsFunction: () -> Unit = { 
    // foo function body
}

如何实现...

  1. 调用 setOnClickListener 函数,并传递一个空的 OnClickListener 实例作为 lambda 表达式:
class RegistrationScreen : Activity() {
    private val submitButton: Button by lazy { findViewById(R.id.submit_button) }  

    override fun onCreate(savedInstanceState: Bundle?) {
        submitButton.setOnClickListener { view: View ->
            // do something on click
        }
    }
}
  1. 在函数体内部修改 submitButton 实例的可见性:
class RegistrationScreen : Activity() {
    private val submitButton: Button by lazy { findViewById(R.id.submit_button) }  

    override fun onCreate(savedInstanceState: Bundle?) {
        submitButton.setOnClickListener {
            submitButton.visibility = View.INVISIBLE
        }
    }
}

它是如何工作的...

由于将 OnClickListener 作为函数处理,我们能够以简洁的 lambda 表达式形式实现它。lambda 的主体将在用户点击按钮时全局调用。在我们的例子中,一旦按钮被点击,它将被隐藏起来。

Lambda 表达式是语言中最基本的函数特性之一,在标准库组件中被广泛使用。它们可以看作是函数或函数式接口实现的缩写形式。Lambda 帮助正确组织代码并减少大量样板代码。lambda 表达式的语法可以看作是放置在 { } 符号之间的代码块。Lambda 表达式可以显式定义函数参数,例如:

val myFunction: (View) -> Unit = { view ->
   view.visibility = View.INVISIBLE
}

为了简洁起见,可以省略显式参数。然而,我们仍然可以使用 it 修饰符来访问它:

val myFunction: (View) -> Unit = { 
   it.visibility = View.INVISIBLE
}

还有更多...

当与 lambda 一起工作时,每次我们想要执行它们体内的代码时,我们都需要在它们上调用 invoke() 函数或其等效的 () 操作符:

val callback: () -> Unit = { println("The job is done!") }
callback.invoke()
callback()

上述代码将打印文本两次:

"The job is done!"
"The job is done!"

还有另一种将函数作为参数传递给其他函数的简洁方式。我们可以使用函数引用来完成:

fun hideView(view: View): Unit  {
    view.visibility = View.INVISIBLE
}

submitButton.setOnClickListener(::hideView)

函数引用方法在代码库中重用函数实现时特别有用。

发现基本的作用域函数 – let, also, apply

在这个菜谱中,我们将探索标准库中的三个有用的扩展函数—letalsoapply。它们与 lambda 表达式配合得很好,有助于编写干净和安全的代码。我们将练习它们的用法,同时将它们应用于实现一系列数据处理操作。

准备工作

假设我们可以使用以下函数获取日期:

fun getPlayers(): List<Player>? 

这里,Player 类是这样定义的:

data class Player(val name: String, val bestScore: Int)

我们希望对 getPlayers() 函数的结果执行以下操作序列:

  1. 将列表中的原始玩家集打印到控制台

  2. 按降序对 Player 对象集合进行排序

  3. Player 集合转换为由 Player.name 属性获取的字符串列表

  4. 将集合限制为第一个元素并打印到控制台

为了完成这个任务,首先,我们需要熟悉 letalsoapply 函数的特性。它们作为泛型类型的扩展函数提供在标准库中。让我们探索 letalsoapply 函数的头部:

public inline fun <T, R> T.let(block: (T) -> R): R

public inline fun <T> T.also(block: (T) -> Unit): T

public inline fun <T> T.apply(block: T.() -> Unit): T 

它们看起来很相似,然而,在返回类型和参数方面有一些细微的差异。以下表格比较了这三个函数:

函数 返回类型 块参数中的参数 块参数定义
let R (从块体) 显式 it (T) -> R
also T (this) 显式 it (T) -> Unit
apply T (this) 隐式 this T.() -> Unit

如何做到这一点...

  1. let 函数与安全运算符一起使用以确保空安全:
getPlayers()?.let {}
  1. let 函数的 lambda 参数块内部,使用 also() 函数将列表中的原始玩家集打印到控制台:
getPlayers()?.let {
 it.also {
        println("${it.size} players records fetched")
 println(it)
 }
}
  1. 使用 let() 函数执行排序和映射转换:
getPlayers()?.let {
    it.also {
        println("${it.size} players records fetched")
        println(it)
    }.let {
        it.sortedByDescending { it.bestScore }
    }
  1. 使用 let() 函数将玩家集合限制为具有最高分数的单个 Player 实例:
getPlayers()?.let {
    it.also {
        println("${it.size} players records fetched")
        println(it)
    }.let {
        it.sortedByDescending { it.bestScore }
    }.let {
        it.first()
 }
  1. 将最佳玩家的名字打印到控制台:
getPlayers()?.let {
    it.also {
        println("${it.size} players records fetched")
        println(it)
    }.let {
        it.sortedByDescending { it.bestScore }
    }.let {
        it.first()
    }.apply {
        val name = this.name
        print("Best Player: $name")
    }
}

它是如何工作的...

让我们测试我们的实现。为了测试的目的,我们可以假设 getPlayers() 函数返回以下结果:


fun getPlayers(): List<Player>? = listOf(
        Player("Stefan Madej", 109),
        Player("Adam Ondra", 323),
        Player("Chris Charma", 239))

我们实现的代码将打印以下输出到控制台:

3 players records fetched
[Player(name=Stefan Madej, bestScore=109), Player(name=Adam Ondra, bestScore=323), Player(name=Chris Charma, bestScore=239)]
Best Player: Adam Ondra

注意,在 apply() 函数的情况下,在函数 lambda 块内部访问类属性和函数时,我们可以省略 this 关键字:

apply {
    print("Best Player: $name")
}

这只是为了在示例代码中提高清晰度而使用。

let() 函数的有用特性是它可以用来确保给定对象的空安全。在以下 let 范围内的示例中,即使某些后台线程试图修改可变 results 变量的原始值,players 参数也始终持有非空值:

var result: List<Player>? = getPlayers()
result?.let { players: List<Player> ->
    ...
}

参见

  • 如果你想了解更多关于 lambda 表达式的内容,请查看 有效使用 lambda 表达式 菜谱

使用 run 范围函数以干净的方式初始化对象

在这个菜谱中,我们将探索标准库提供的另一个有用的扩展函数,称为 run()。我们将使用它来创建和设置 java.util.Calendar 类的实例。

准备工作

首先,让我们通过以下函数头探索标准库中定义的 run() 函数的特性:

public inline fun <T, R> T.run(block: T.() -> R): R

它被声明为一个泛型类型的扩展函数。run函数在block参数内部提供了隐式的this参数,并返回block执行的返回值。

如何实现...

  1. 声明Calendar.Builder类的实例并将其run()函数应用于它:
val calendar = Calendar.Builder().run {
    build()
}
  1. 将所需的属性添加到构建器中:
val calendar = Calendar.Builder().run {
    setCalendarType("iso8601")
 setDate(2018, 1, 18)
 setTimeZone(TimeZone.getTimeZone("GMT-8:00"))
    build()
}
  1. 将日历中的日期打印到控制台:
val calendar = Calendar.Builder().run {
    setCalendarType("iso8601")
    setDate(2018, 1, 18)
    setTimeZone(TimeZone.getTimeZone("GMT-8:00"))
    build()
}
print(calendar.time)

它是如何工作的...

run函数应用于Calendar.Builder实例。在传递给run函数的 lambda 内部,我们可以通过this修饰符访问Calendar.Builder的属性和方法。换句话说,在run函数块内部,我们正在访问Calendar.Builder实例的作用域。在食谱代码中,我们省略了使用this关键字调用Builder方法。我们可以直接调用它们,因为run函数允许通过隐式的this修饰符在其作用域内访问Builder实例。

还有更多...

我们还可以将run()函数与安全的?运算符一起使用,以提供run()函数作用域内this关键字引用的对象的 null 安全性。您可以在以下配置 Android WebView类的示例中看到它的实际应用:

webview.settings?.run {
    this.javaScriptEnabled = true
    this.domStorageEnabled = false
}

在前面的代码片段中,我们确保在run函数的作用域内settings属性不为 null,并且我们可以通过this关键字访问它。

相关内容

  • Kotlin 标准库提供了另一个类似的扩展函数,称为apply(),它对于对象的初始化非常有用。主要区别在于它返回调用它的对象的原始实例。您可以在第五章的“以美味的方式实现构建器”食谱中探索它,采用 Kotlin 概念的优雅设计模式

使用高阶函数

Kotlin 被设计为提供对函数的一等支持。例如,我们能够轻松地将函数作为参数传递给另一个函数。我们还可以创建一个可以返回另一个函数的函数。这种类型的函数称为高阶函数。这个强大的功能有助于轻松编写函数式风格的代码。返回函数而不是值的能力,加上将函数实例作为参数传递给另一个函数的能力,使得延迟计算和清晰塑造代码成为可能。在本食谱中,我们将实现一个辅助函数,该函数将测量传递给它作为参数的其他函数的执行时间。

如何实现...

实现measureTime函数:

fun measureTime(block: () -> Unit): Long {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()

    return end - start
}

它是如何工作的...

measureTime()函数接受一个名为block的函数类型参数。block函数参数在measureTime()函数内部使用()修饰符调用。最后,返回时间戳(在块执行前后)之间的差异。

让我们分析以下示例,展示measureTime()函数的实际应用。我们可以考虑以下函数负责计算给定整数的阶乘:

fun factorial(n: Int): Long {
    sleep(10)
    return if (n == 1) n.toLong() else n * factorial(n - 1)
}

为了测量factorial()函数的执行时间,我们可以使用measureTime()函数如下:

val duration = measureTime {
    factorial(13)
}
print("$duration ms")

结果,我们得到打印到控制台上的执行时间:

154 ms

注意,也可以将函数引用而不是 lambda 实例作为measureTime()函数的参数传递:

fun foo() = sleep(1000)
val duration = measureTime(::foo)
print("$duration ms")

函数柯里化

柯里化是函数式编程中的一种常见技术。它允许将给定的多参数函数转换为一串函数,每个函数只有一个参数。每个生成的函数处理原始(未柯里化)函数的一个参数,并返回另一个函数。

在这个菜谱中,我们将实现一个可以应用于任何接受三个参数的函数的自动柯里化机制。

准备工作

为了理解函数柯里化的概念,让我们考虑以下处理三个参数的函数的示例:

fun foo(a: A, b: B, c: C): D 

它的柯里化形式看起来是这样的:

fun carriedFoo(a: A): (B) -> (C) -> D 

换句话说,foo函数的柯里化形式将接受一个A类型的单个参数,并返回一个以下类型的函数:(B) -> (C) -> D。返回的函数负责处理原始函数的第二个参数,并返回另一个函数,该函数接受第三个参数并返回一个类型为D的值。

在下一节中,我们将实现curried()扩展函数,该函数用于以下声明的泛型函数类型:<P1, P2, P3, R> ((P1, P2, P3))curried()函数将返回一个单参数函数链,并将适用于任何接受三个参数的函数。

如何做到这一点...

  1. 声明curried()函数的标题:
fun <P1, P2, P3, R> ((P1, P2, P3) -> R).curried(): (P1) -> (P2) -> (P3) -> R 
  1. 实现函数curried()的主体:
fun <P1, P2, P3, R> ((P1, P2, P3) -> R).curried(): (P1) -> (P2) -> (P3) -> R =
        { p1: P1 ->
            { p2: P2 ->
                { p3: P3 ->
                    this(p1, p2, p3)
 }
             }
        }

它是如何工作的...

让我们探索如何在实际中使用curried()函数。在以下示例中,我们将对以下函数实例调用curried(),该实例负责计算三个整数的和:

fun sum(a: Int, b: Int, c: Int): Int = a + b + c

为了获得sum()函数的柯里化形式,我们必须在它的引用上调用curried()函数:

::sum.curried()

然后,我们可以以以下方式调用柯里化的求和函数:

val result: Int = ::sum.curried()(1)(2)(3)

最后,result变量将被分配一个等于6的整数值。

为了调用curried()扩展函数,我们使用::修饰符访问sum()函数引用。然后,我们依次调用由柯里化函数返回的函数序列中的下一个函数。

之前的代码可以用更冗长的形式来写,带有显式的类型声明:

val sum3: (a: Int) -> (b: Int) -> (c: Int) -> Int = ::sum.curried()
val sum2: (b: Int) -> (c: Int) -> Int = sum3(1)
val sum1: (c: Int) -> Int = sum2(2)
val result: Int = sum1(3)

在幕后,柯里化机制的实现只是返回嵌套在彼此内部的函数。每次调用特定函数时,它都会返回另一个具有减少一个参数的函数。

还有更多...

有一个类似的模式称为部分应用。它比柯里化更灵活,因为它不限制每个函数处理的参数数量。例如,给定一个如下声明的foo函数:

fun foo(a: A, b: B, c: C): D 

我们可以将其转换为以下形式:

fun foo(a: A, c: C) -> (B) -> D

当我们无法在当前作用域中为函数提供所需的所有参数时,柯里化和部分应用都非常有用。我们可以只将可用的参数应用到函数上,并返回转换后的函数。

函数组合

函数柯里化配方中,我们发现了一种巧妙的方法,可以从函数中提取新的函数。在这个配方中,我们将实现相反的转换。有一个选项可以合并多个现有函数的声明,并从中定义一个新的函数,这是一种常见的函数式编程模式,称为函数组合。Kotlin 没有提供内置的函数组合机制。然而,多亏了对函数类型操作扩展的内置支持,我们能够手动实现一个可重用的组合机制。

准备工作

为了熟悉函数组合,让我们研究以下示例。假设我们定义了以下函数:

fun length(word: String) = word.length
fun isEven(x:Int): Boolean = x.rem(2) == 0

第一个函数负责返回给定字符串的长度。第二个函数检查给定的整数是否为偶数。为了定义一个基于这两个函数的新函数,我们可以进行嵌套函数调用:

fun isCharCountEven(word: String): Boolean = isEven(length(word))

然而,如果我们能够对函数引用进行操作,那就更好了。为了使其更加简洁,我们希望能够使用以下语法声明isCharCountEven()函数,用于函数组合:

val isCharCountEven: (word: String) -> Boolean = ::length and ::isEven

如何实现...

  1. 声明一个名为and()的单参数函数的infix扩展函数:
infix fun <P1, R, R2> ((P1) -> R).and(function: (R) -> R2): (P1) -> R2 = {

}
  1. 内部调用基本函数和and()函数的参数:
infix fun <P1, R, R2> ((P1) -> R).and(function: (R) -> R2): (P1) -> R2 = {
    function(this(it))
}

它是如何工作的...

为了探索我们的函数组合实现,让我们使用and()函数,通过length()属性和isEven()函数组合isCharCountEven()函数:

fun length(word: String) = word.length
fun isEven(x:Int): Boolean = x.rem(2) == 0
val isCharCountEven: (word: String) -> Boolean = ::length and ::isEven
print(isCharCountEven("pneumonoultramicroscopicsilicovolcanoconiosis"))

上述代码将返回以下输出:

false

在底层,and()扩展函数只是依次调用给定的两个函数。然而,多亏了中缀表示法,我们可以在代码中执行组合,同时避免嵌套函数调用。此外,前一个示例中::length and ::isEven调用的结果返回一个新的函数实例,它可以像普通函数一样轻松重用。

实现 Either Monad 设计模式

Monad 的概念是函数式编程设计模式之一。我们可以将 Monad 理解为对数据类型的封装,它为该数据类型添加特定的功能或为封装对象的不同的状态提供自定义处理程序。最常用的之一是 Maybe monad。Maybe monad 旨在提供有关封装属性存在的信息。它可以在属性可用时返回封装类型的实例,在不可用时返回空值。Java 8 引入了Optional<T>类,它实现了 Maybe 概念。这是一种避免在 null 值上操作的好方法。

然而,除了有关于不可用状态的信息之外,我们通常还希望能够提供一些额外的信息。例如,如果服务器返回一个空响应,那么获取一个错误代码或消息而不是null或空响应字符串将是有用的。这是一个适用于另一种类型 Monad 的场景,通常称为Either,我们将在本食谱中实现它。

如何操作...

  1. Either声明为一个sealed类:
sealed class Either<out E, out V>
  1. 添加两个Either的子类,分别表示错误和值:
sealed class Either<out L, out R> {
    data class Left<out L>(val left: L) : Either<L, Nothing>()
    data class Right<out R>(val right: R) : Either<Nothing, R>()
}
  1. 添加用于方便实例化Either的工厂函数:
sealed class Either<out L, out R> {
    data class Left<out L>(val left: L) : Either<L, Nothing>()
    data class Right<out R>(val right: R) : Either<Nothing, R>()

    companion object {
        fun <R> right(value: R): Either<Nothing, R> = 
         Either.Right(value)
        fun <L> left(value: L): Either<L, Nothing> = 
         Either.Left(value)
    }
}

它是如何工作的...

为了使用Either类并从中受益于Either.right()Either.left()方法,我们可以实现一个getEither()函数,该函数将尝试执行作为参数传递给它的某些操作。如果操作成功,它将返回包含操作结果的Either.Right实例,否则,它将返回Either.Left,包含一个抛出的异常实例:

fun <V> getEither(action: () -> V): Either<Exception, V> =
        try { Either.right(action()) } catch (e: Exception) { Either.left(e) }

按照惯例,我们使用Either.Right类型来提供默认值,而使用Either.Left来处理任何可能的边缘情况。

还有更多...

Either Monad 可以提供的一个基本函数式编程特性,是能够对其值应用函数。我们可以简单地通过fold()函数扩展Either类,该函数可以接受两个函数作为参数。第一个函数应用于Either.Left类型,第二个函数应用于Either.Right

sealed class Either<out L, out R> {
    data class Left<out L>(val left: L) : Either<L, Nothing>()
    data class Right<out R>(val right: R) : Either<Nothing, R>()

    fun <T> fold(leftOp: (L) -> T, rightOp: (R) -> T): T = when (this) {
        is Left -> leftOp(this.left)
        is Right -> rightOp(this.right)
    }

  //...
}

fold()函数将从leftOprightOp函数返回一个值,取决于使用的是哪一个。我们可以通过一个服务器请求解析示例来说明fold()函数的用法。

假设我们声明了以下类型:

data class Response(val json: JsonObject)
data class ErrorResponse(val code: Int, val message: String)

我们还有一个负责提供后端响应的函数:

fun someGetRequest(): Either<ErrorResponse, Response> = //..

我们可以使用fold()函数以正确的方式处理返回值:

someGetRequest().fold({
    showErrorInfo(it.message)
}, {
    parseAndDisplayResults(it.json)
})

我们还可以通过其他有用的函数扩展Either类,这些函数类似于标准库中用于数据处理操作的可用的函数——mapfilterexists

自动函数记忆化的方法

缓存是一种用于通过缓存昂贵函数调用的结果并在需要时重新使用它们的准备好的值来优化程序执行速度的技术。尽管缓存在内存使用和计算时间之间造成明显的权衡,但通常提供所需的性能至关重要。通常,我们将此模式应用于计算昂贵的函数。它可以帮助优化多次以相同参数值调用自身的递归函数。缓存可以轻松地添加到函数实现中。然而,在这个菜谱中,我们将创建一个通用、可重用的缓存机制,可以应用于任何函数。

如何实现...

  1. 声明一个负责缓存结果的Memoizer类:
class Memoizer<P, R> private constructor() {

    private val map = ConcurrentHashMap<P, R>()

    private fun doMemoize(function: (P) -> R):
        (P) -> R = { param: P ->
        map.computeIfAbsent(param) { param: P ->
                    function(param)
                }
            }

    companion object {
        fun <T, U> memoize(function: (T) -> U): (T) -> U =
                Memoizer<T, U>().doMemoize(function)
    }
}
  1. (P) -> R函数类型提供一个memoized()扩展函数:
fun <P, R> ((P) -> R).memoized(): (P) -> R = Memoizer.memoize<P, R>(this)

它是如何工作的...

memoize()函数接受一个单参数函数的实例作为其参数。Memoizer类包含ConcurrentHashmap<P, R>实例,用于缓存函数的返回值。该映射存储传递给memoize()作为参数的函数作为键,并将它们的返回值作为其值。首先,memoize()函数查找传递给函数作为参数的特定参数的值。如果该值存在于映射中,则返回。否则,执行函数,并将结果既通过memoize()返回,又放入映射中。这是通过标准库提供的方便的inline fun <K, V> ConcurrentMap<K, V>.computeIfAbsent(key: K, defaultValue: () -> V): V扩展函数实现的。

此外,我们还提供了一个Function1类型的扩展函数memoized(),允许我们直接将memoize()函数应用于函数引用。

Kotlin 中的底层函数被编译成 Java 字节码中的FunctionN接口实例,其中N对应函数参数的数量。正因为这个事实,我们能够为函数声明一个扩展函数。例如,为了为接受两个参数的函数(P, Q)-> R添加一个扩展函数,我们需要定义一个扩展函数为fun <P, Q, R> Function2<P, Q, R>.myExtension(): MyReturnType

现在,让我们看看我们如何从memoized()函数的实际应用中受益。让我们考虑一个递归计算整数的阶乘的函数:

fun factorial(n: Int): Long = if (n == 1) n.toLong() else n * factorial(n - 1)

我们可以将memoized()扩展函数应用于启用结果缓存:

val cachedFactorial = ::factorial.memoized()
println(" Execution time: " + measureNanoTime { cachedFactorial(12) } + " ns")
println(" Execution time: " + measureNanoTime { cachedFactorial(13) } + " ns")

以下代码在标准计算机上给出以下输出:

Execution time: 1547274 ns
Execution time: 24690 ns

如您所见,尽管第二次计算需要更多的factorial()函数递归调用次数,但它比第一次计算花费的时间少得多。

还有更多...

我们可以为接受超过一个参数的其他函数实现类似的自动记忆化实现。为了声明一个接受N个参数的扩展函数,我们需要为FunctionN类型实现一个扩展函数。

第四章:强大的数据处理

在本章中,我们将介绍以下菜谱:

  • 简单地组合和消费集合

  • 过滤数据集

  • 自动移除null

  • 使用自定义比较器排序数据

  • 根据数据集元素构建字符串

  • 将数据划分为子集

  • 使用mapflatMap转换数据

  • 数据集折叠和缩减

  • 数据分组

简介

本章重点介绍标准库对声明式操作集合的支持。以下菜谱展示了针对数据集转换、缩减和分组的不同类型编程问题的解决方案。我们将学习如何以函数式编程风格处理数据处理操作,以及标准库中内置的强大数据处理扩展。

简单地组合和消费集合

Kotlin 标准库提供了一些方便的扩展,使得集合创建和合并变得简单且安全。我们将逐步学习它们。假设我们定义了以下Message类:

data class Message(val text: String, 
                   val sender: String, 
                   val timestamp: Instant = Instant.now())

在这个菜谱中,我们将创建两个包含Message实例的样本集合,并将它们合并成一个Message对象的列表。接下来,我们将遍历消息列表,并将它们的文本打印到控制台。

准备工作

Kotlin 标准库提供了两个基本接口,用于表示集合数据结构——CollectionMutableCollection,两者都扩展了Iterable接口。第一个接口定义了一个不可变集合,它只支持对其元素的读取访问。第二个接口允许我们添加和删除元素。还有更多专门化的接口扩展了CollectionMutableCollection基本类型,例如ListMutableListSetMutableSet

有许多函数可用于创建不同类型的集合。最常用的函数是<T> listOf(vararg elements: T),它实例化一个List实例,以及<T> mutableListOf(vararg elements: T),它返回一个MutableList实例,作为函数参数给出的元素。

如何做到这一点...

  1. 让我们声明两个包含样本数据的列表:
val sentMessages = listOf (
    Message("Hi Agat, any plans for the evening?", "Samuel"),
    Message("Great, I'll take some wine too", "Samuel")
)
val inboxMessages = mutableListOf(
        Message("Let's go out of town and watch the stars tonight!",
         "Agat"),
        Message("Excelent!", "Agat")
)
  1. sentMessagesinboxMessages合并到一个集合中:
val allMessages: List<Message> = sentMessages + inboxMessages
  1. 将存储在allMessages列表中的Message对象的文本打印到控制台:
val allMessages: List<Message> = sentMessages + inboxMessages
allMessages.forEach { (text, _) ->
    println(text)
}

它是如何工作的...

因此,我们的代码将打印以下文本到控制台:

Hi Agat, any plans for the evening?
Great, I'll take some wine too
Let's go out of town and watch the stars tonight!
Excelent!

为了将一个集合的元素添加到另一个集合中,我们使用了 + 操作符。标准库通过合并两个 Collection 类型实例集合的元素到单个实例的代码来重载此操作符。sentMessagesinboxMessages 变量被声明为 List 实例。plus 函数返回一个新的 Collection 实例,包含 sentMessagesinboxMessages 列表中的元素。最后,我们使用 forEach() 函数遍历列表的下一个元素。在传递给 forEach 函数的 lambda 块中,我们将当前 Messagetext 属性打印到控制台。我们在 println() 函数内部直接访问 lambda 的 Message 类型参数的文本属性。

更多内容...

标准库还为 Collection 类型重载了 - 操作符。我们可以用它从集合中减去一些元素。例如,我们可以这样使用它:

val receivedMessages = allMessages - sentMessages
receivedMessages.forEach { (text, _) ->
    println(text)
}

我们将得到以下输出:

Let's go out of town and watch the stars tonight!
Excelent!

我们也可以使用标准的 for 循环来实现迭代:

for (msg in allMessages) {
    println(msg.text)
}

相关内容

  • 你可以在第二章 Expressive Functions and Adjustable Interfaces 中的 Destructuring types 菜单中了解更多关于解构声明的信息,第二章。

  • 如果你想掌握 lambda 表达式,可以查看第三章 Working effectively with lambdas and closures 中的 Shaping Code with Kotlin Functional Programming Features 菜单,第三章。

过滤数据集

过滤是数据处理领域中最常见的编程挑战之一。在本菜谱中,我们将探索标准库的内置扩展函数,这些函数提供了一种简单的方式来过滤 Iterable 数据类型。假设我们有一个以下 Message 类声明:

data class Message(val text: String,
                   val sender: String,
                   val receiver: String,
                   val folder: Folder = Folder.INBOX,
                   val timestamp: Instant = Instant.now())

enum class Folder { INBOX, SENT }

getMessages() 函数返回以下数据:

fun getMessages() = mutableListOf(
        Message("Je t'aime", "Agat", "Sam", Folder.INBOX),
        Message("Hey, Let's go climbing tomorrow", "Stefan", "Sam", Folder.INBOX),
        Message("<3", "Sam", "Agat", Folder.SENT),
        Message("Yeah!", "Sam", "Stefan", Folder.SENT)
)

我们将对 getMessages() 函数应用一个过滤操作,只返回具有 Folder.INBOX 属性和 sender 属性等于 Agat 的消息。

准备工作

为了实现过滤转换,我们将使用标准库提供的 Iterable<T>.filter(predicate: (T) -> Boolean) 扩展函数。filter() 函数接受一个返回 truefalse 值的谓词函数,用于给定泛型 Iterable 数据集类型 T 的元素。

如何操作...

  1. getMessages() 函数应用过滤:
getMessages().filter { it.folder == Folder.INBOX && it.sender == "Agat" }
  1. 遍历过滤后的消息并将它们的消息打印到控制台:
getMessages().filter { it.folder == Folder.INBOX && it.sender == "Agat" }
 .forEach { (text) ->
     println(text)
 }

工作原理...

我们正在将 filter 函数应用于 ge``tMessages() 函数的结果。我们将一个 lambda 块传递给 filter 函数,该函数为列表的每个元素返回一个布尔值。filter 函数返回一个包含过滤对象的列表。最后,我们使用 forEach() 函数遍历列表的下一个元素。在传递给 forEach 函数的 lambda 块中,我们将当前 Messagetext 属性打印到控制台。

因此,上一节中的代码将在控制台打印以下输出:

Je t'aime

还有更多...

Kotlin 标准库为其他类型提供了相应的 filter() 扩展函数,例如 ArraySequenceMap。还有许多过滤函数的变体,这些变体可以用于特定场景。您可以在 Kotlin 标准库 kotlin.collections 包的官方文档中找到它们,网址为 kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections

参见

  • 如果你想掌握 lambda 表达式,你可以查看第三章 有效地使用 lambda 和闭包 菜谱,来自 使用 Kotlin 函数式编程特性塑造代码

自动移除空值

当与服务器或外部库的糟糕设计 API 一起工作时,我们经常需要处理接收空返回值的情况。幸运的是,有一些标准库特性允许我们有效地处理空值。在本菜谱中,我们将实现一个数据预处理操作,该操作将自动从数据集中移除所有空值。假设我们正在与一个提供最新新闻源的外部 API 一起工作。不幸的是,它不是空安全的,可能会返回随机的空值。例如,让我们假设我们有一个 getNews(): List<News> 函数,它返回以下数据:

fun getNews() = listOf(
 News("Kotlin 1.2.40 is out!", "https://blog.jetbrains.com/kotlin/"),
 News("Google launches Android KTX Kotlin extensions for developers",
 "https://android-developers.googleblog.com/"),
 null,
 null,
 News("How to Pick a Career", "waitbutwhy.com")
)

News 类定义如下:

data class News(val title: String, val url: String)

如何做到...

filterNotNull 函数应用于 getNews() 函数:

getNews()
        .filterNotNull()
        .forEachIndexed { index, news ->
            println("$index. $news")
        }

它是如何工作的...

因此,我们将得到以下输出打印到控制台:

0\. News(title=Kotlin 1.2.40 is out!, url=https://blog.jetbrains.com/kotlin/)
1\. News(title=Google launches Android KTX Kotlin extensions for developers, url=https://android-developers.googleblog.com/)
2\. News(title=How to Pick a Career, url=waitbutwhy.com)

相比之下,没有 filterNotNull() 函数的代码如下:

getNews().forEachIndexed { index, news ->
    println("$index. ${news.toString()}")
}

这将在控制台打印以下输出:

0\. News(title=Kotlin 1.2.40 is out!, url=https://blog.jetbrains.com/kotlin/)
1\. News(title=Google launches Android KTX Kotlin extensions for developers, url=https://android-developers.googleblog.com/)
2\. null
3\. null
4\. News(title=How to Pick a Career, url=waitbutwhy.com)

Iterable.filterNotNull() 扩展函数从原始数据集中移除所有空值。在底层,它将非空值复制到新创建的 List 实例中。这就是为什么在处理大型数据集时,使用序列而不是集合以提供延迟求值更有效的原因。

参见

  • 过滤数据集 菜谱中,我们探讨了如何使用标准库提供的基本 filter() 函数

  • 如果你想掌握 lambda 表达式,你可以查看第三章的有效使用 lambda 和闭包菜谱,使用 Kotlin 函数式编程特性塑造代码

使用自定义比较器进行数据排序

在这个菜谱中,我们将探索按集合元素属性对集合元素进行排序的支持。

开始

假设我们正在处理以下声明的两个Message类型集合:

data class Message(val text: String,
                   val sender: String,
                   val receiver: String,
                   val time: Instant = Instant.now())

这些由allMessages变量提供:

val sentMessages = listOf(
        Message("I'm programming in Kotlin, of course", 
                "Samuel", 
                "Agat", 
                Instant.parse("2018-12-18T10:13:35Z")),
        Message("Sure!", 
                "Samuel", 
                "Agat", 
                Instant.parse("2018-12-18T10:15:35Z"))
)
val inboxMessages = mutableListOf(
        Message("Hey Sam, any plans for the evening?", 
                "Samuel", 
                "Agat", 
                Instant.parse("2018-12-18T10:12:35Z")),
        Message("That's cool, can I join you?", 
                "Samuel", 
                "Agat",
                Instant.parse("2018-12-18T10:14:35Z"))
)
val allMessages = sentMessages + inboxMessages

如果我们打印出allMessages列表中连续消息的文本,我们将在控制台得到以下文本输出:

I'm learning Kotlin, of course
Sure!
Hey Sam, any plans for the evening?
That's cool, can I join you?

这看起来不太对。消息应该按时间顺序显示。这意味着它们应该按time属性排序。

如何做...

  1. sortedBy函数应用于allMessages集合:
allMessages.sortedBy { it.time }
  1. 将排序后的元素打印到控制台:
allMessages.sortedBy { it.time }
        .forEach {
            println(it.text)
 }

它是如何工作的...

如果我们运行前面的代码,我们得到以下输出:

I'm programming in Kotlin, of course
Sure!
Hey Sam, any plans for the evening?
That's cool, can I join you?

现在,所有消息都已正确排序,对话也变得有意义。

还有更多...

如果我们的集合由实现 Comparable 接口的对象组成,我们就可以通过对其应用sorted()函数来简单地对其进行排序。Kotlin 标准库还提供了sortedBy()函数的更多专用版本,例如sortedByDescending()sortedWith()。第一个是一个基础排序函数,但它返回按相反顺序排序的数据集。sortedWith()函数允许我们使用自定义比较器对列表进行排序。例如,为了按sender属性首先排序,然后按time属性排序Message类型元素的集合,我们可以编写以下代码:

allMessages.sortedWith(compareBy({it.sender}, {it.time}))

基于数据集元素构建字符串

有时候,我们会遇到根据集合元素生成文本的问题。这就是Iterable.joinToString()扩展函数能帮到的地方。例如,我们可以考虑实现一个电子邮件转发功能。当用户点击转发按钮时,原始消息的正文文本被连接起来,前缀看起来像这样:

<br/>
<p>---------- Forwarded message ----------</p>
<p>
From: johny.b@gmail.com <br/>
Date: 14/04/2000 <br/>
Subject: Any plans for the evening?<br/>
To: natasha@gmail.com, barbra@gmail.com<br/>
</p>

在这个菜谱中,我们将实现一个函数,该函数将生成收件人的字符串,例如:

To: natasha@gmail.com, barbra@gmail.com</br>

对于给定的Address类型对象列表,它定义如下:

data class Address(val emailAddress: String, val displayName: String)

如何做...

  1. 声明generateRecipientsString()函数头:
fun generateRecipientsString(recipients: List<Address?>): String
  1. 首先从recipient参数中移除所有null项:
fun generateRecipientsString(recipients: List<Address?>): String =
 recipients.filterNotNull()
  1. Address类型的集合元素转换为与Address.emailAddress属性对应的String类型元素:
fun generateRecipientsString(recipients: List<Address?>): String =
        recipients.filterNotNull()
 .map { it.emailAddress }
  1. 为了将集合元素合并成字符串,应用joinToString()函数:
fun generateRecipientsString(recipients: List<Address?>): String =
        recipients.filterNotNull()
                .map { it.emailAddress }
 .joinToString(", ", "To: ", "<br/>")

它是如何工作的...

generateRecipientsString()函数使用标准库kotlin.collections包中的Iterable.joinToString()扩展函数来生成输出字符串。joinToString()函数接受三个参数——分隔符字符,用于连接下一个子字符串,前缀和后缀字符串。它在一个字符串值的集合上调用。我们还应用了预处理操作,负责从Address对象的列表中删除null值并将Address类型映射到与Address.emailAddress属性对应的String

还有更多...

我们还可以使用joinToString()函数的另一个版本来简化generateRecipientsString()函数实现中的逻辑:

fun generateRecipientsString(recipients: List<Address?>): String =
        recipients.filterNotNull()
            .joinToString(", ", "To: ", "<br/>") { it.emailAddress }

如您所见,它采用额外的形式为内联 lambda 块的形式的参数,该参数充当应用于每个recipients集合元素的转换函数。

参见

  • 要深入了解数据集映射操作,您可以阅读使用 map 和 flatMap 进行数据转换菜谱

将数据划分为子集

常见的数据处理任务是将数据集合划分为子集。在这个菜谱中,我们将探索标准库函数,这些函数允许我们将集合缓冲到更小的块中。假设我们有一个包含大量Message类型对象的列表,我们希望将其转换成固定大小的子列表集合。例如,转换将原始集合的n个元素:

[mssg_1, mssg_2, mssg_3, mssg_4, mssg_5, mssg_6, mssg_7, ..., mssg_n]

然后将其分割成包含四个元素子集的集合:

[[mssg_1, mssg_2, mssg_3, mssg_4], ..., [mssg_n-3, mssg_n-2, mssg_n-1, mssg_n]]

准备工作

让我们先声明一个Message类,我们将在下面的菜谱中使用它:

data class Message(val text: String,
                   val time: Instant = Instant.now())

让我们声明一个存储样本数据的messages变量:

val messages = listOf(
        Message("Any plans for the evening?"),
        Message("Learning Kotlin, of course"),
        Message("I'm going to watch the new Star Wars movie"),
        Message("Would u like to join?"),
        Message("Meh, I don't know"),
        Message("See you later!"),
        Message("I like ketchup"),
        Message("Did you send CFP for Kotlin Conf?"),
        Message("Sure!")
)

如何做...

  1. windowed()函数应用于messages列表:
val pagedMessages = messages.windowed(4, partialWindows = true, step = 4)  
  1. 将一个transform: (List<T>) -> R转换函数作为额外的内联参数添加到windowed函数中:
val pagedMessages = messages.windowed(4, partialWindows = true, step = 4) { 
    it.map { it.text }
}

它是如何工作的...

windowed函数将原始消息列表分割成指定大小的子列表。结果,我们得到List<List<Message>>类型分配给pagedMessages处理。我们可以使用以下代码打印下一个消息子集:

pagedMessages.forEach { println(it) }

结果,我们得到以下输出打印到控制台:

[Any plans for the evening?, Learning Kotlin, of course, I'm going to watch the new Star Wars movie, Would u like to join?]
[Meh, I don't know, See you later!, I like the ketchup, Did you send CFP for Kotlin Conf?]
[Sure!]

windowed函数接受四个参数——窗口大小、一个标志表示是否应创建部分窗口、一个步长值以及一个可选的转换函数,该函数负责转换生成的每个窗口。在我们的场景中,我们使用窗口大小等于4。这就是为什么我们需要将步长值也指定为4,因为我们希望将连续的Message实例存储在下一个窗口中。我们还设置了partialWindows参数为true,否则包含单个消息的最后一个窗口将被省略。windowed函数的最后一个参数允许我们将每个窗口映射到另一个类型。我们将windowed()函数返回的每个子列表映射到List<String>类型。还有一个windowed函数的另一个版本,没有最后一个映射参数,因此它可以被视为可选的。

还有更多...

还提供了一个windowed()函数的便捷包装器,称为chunked()。它不需要步长参数,并自动将其设置为窗口大小值。它非常适合这个问题的解决方案,然而,windowed()函数被解释为更基础的。

参见

使用 map 和 flatMap 进行数据转换

对声明式数据映射操作的支持是函数式数据处理领域的基本且最强大的功能之一。通常,当处理数据时,我们需要将特定类型的集合转换成另一种类型。从集合的每个元素生成对象并将所有这些新对象合并到目标集合中也是一个常见的场景。这些就是map()flatMap扩展函数能够帮助的场景。

在这个菜谱中,我们将使用这两个函数来实现映射数据转换。让我们想象我们正在处理负责管理大学系讲座的系统的一部分。我们得到了以下类型:

class Course(val name: String, val lecturer: Lecturer, val isPaid: Boolean = false)
class Student(val name: String, val courses: List<Course>)
class Lecturer(val name: String)

我们还有一个getStudents(): List<Student>函数,它从数据库中返回所有学生的列表。我们想要实现getLecturesOfCoursesWithSubscribedStudents()函数,该函数将getStudents()的结果转换成计算一个列表,其中包含至少有一名学生订阅的课程讲师。

如何做到这一点...

  1. 声明一个函数头:
fun getLecturesOfCoursesWithSubscribedStudents()
  1. 对学生列表应用flatMap操作:
fun getLecturesOfCoursesWithSubscribedStudents() =
        getStudents()
                .flatMap { student ->
                    student.courses
                }
  1. 限制集合元素的值为唯一值:
fun getLecturesOfCoursesWithSubscribedStudents() =
        getStudents()
                .flatMap { student ->
                    student.courses
                }
                .distinct()
  1. Course类型元素的集合映射到它们对应的Lecturer类型属性:
fun getLecturesOfCoursesWithSubscribedStudents() =
 getStudents()
 .flatMap { student ->
     student.courses
 } 
 .distinct()
 .map { course -> course.lecturer } 
 .distinct()

它是如何工作的...

以下flatMap操作将getLecturesOfCoursesWithSubscribedStudents()函数将Student类型对象的集合转换为Course类型对象的集合,通过合并Student.courses: Collection<Course>属性中的元素:

getStudents()
        .flatMap { student: Student ->
            student.courses
        }

结果,前面的代码返回了Collection<Course>类型。flatMap操作返回的集合包含所有学生(从getStudents()函数获取)订阅的所有课程的集合。

接下来,为了去除重复的课程,我们使用distinct()函数来追加操作链。然后,我们使用map()函数。它负责将Course类型的每个元素转换为从Course.lecturer属性对应的Lecturer类型。最后,我们再次应用distinct()函数,以返回没有重复的讲师列表。

还有更多...

map()flatMap()扩展函数也适用于Map数据结构类型。当需要将映射转换为从映射的键值对转换的对象列表时,它们非常有用。

数据集的折叠和归约

虽然map()操作符接受给定大小的列表并返回另一个大小相同且类型修改后的列表,但应用于数据集的fold()reduce()操作返回一个单一元素,由数据集的连续元素组成。这听起来可能像使用简单的命令式循环和局部累加变量(它持有当前状态并在每次迭代中更新)的简单场景。我们可以考虑一个简单的任务,即求整数值的总和。假设我们想要计算从010的连续整数的总和。我们可以使用一个简单的for循环来实现它:

var sum = 0
(1..10).forEach {
    sum += it
}

然而,有一种替代的函数式方法可以执行这样的计算,使用fold()函数:

val sum = (1..3).toList().fold(0) { acc, i -> acc + i }

当我们实现一系列函数式数据处理操作时,第二种方法更可取。与 for 循环相比,fold函数不强制显式消费集合元素,并且可以很容易地与其他函数式操作符一起使用。

在这个菜谱中,我们将使用fold函数来实现处理音频专辑曲目时负责的函数。假设我们给出了以下数据类型:

data class Track(val title: String, val durationInSeconds: Int)
data class Album(val name: String, val tracks: List<Track>)

以及示例Album类实例:

val album = Album("Sunny side up", listOf(
        Track("10/10", 176),
        Track("Coming Up Easy", 292),
        Track("Growing Up Beside You", 191),
        Track("Candy", 303),
        Track("Tricks of the Trade", 151)
))

我们想要实现一个针对Album类型的扩展函数,该函数将为作为参数给出的Track返回一个相对起始时间。例如,Growing Up Beside You这首曲子的起始时间应该是 468 秒。

如何实现...

  1. 声明一个针对Album类的扩展函数:
fun Album.getStartTime(track: Track): Int
  1. 计算给定Track参数的起始时间:
fun Album.getStartTime(track: Track): Int {
    val index = tracks.indexOf(track)
 return this.tracks
            .take(index)
 .map { (name, duration) -> duration }
            .fold(0) { acc, i -> acc + i }
}
  1. track参数添加一个安全检查:
fun Album.getStartTime(track: Track): Int {
 if (track !in tracks) throw IllegalArgumentException("Bad 
     track")

    val index = tracks.indexOf(track)
    return tracks
        .take(index)
        .map { (name, duration) -> duration }
        .fold(0) { acc, i -> acc + i }
}

它是如何工作的...

在一开始,我们的函数对传入的track参数进行安全检查,以验证它是否属于当前的Album实例。如果给定的曲目在Album.tracks集合中未找到,则抛出IllegalArgumentException异常。接下来,我们创建一个子列表,只包含tracks属性元素中的从0索引到作为函数参数传递的track索引的元素。这个子列表是通过使用take()操作符创建的。然后,我们将每个Track类型的元素映射到对应的Int类型,即曲目的持续时间。最后,我们应用fold函数,以累加连续Track元素的durationInSeconds属性值。fold函数接受一个initial参数,该参数负责初始化内部accumulator变量,该变量持有折叠结果的当前状态。

在我们的情况下,我们将 0 作为initial值传递,这对应于专辑的起始时间。在传递给fold函数的第二个参数中,我们定义了如何使用每个连续的durationInSeconds值更新accumulator

让我们测试一下Album.getStartTime()函数的实际效果:

println(album.getStartTime(Track("Growing Up Beside You", 191)))
println(album.getStartTime(Track("Coming Up Easy", 292)))

上述代码返回以下输出:

468
176

还有更多...

标准库提供了一个名为reduce()的类似函数,它执行与fold相同的操作。这两个函数之间的区别在于fold需要一个显式的初始值,而reduce则使用列表中的第一个元素作为初始值。

数据分组

Kotlin 标准库为数据集按组分组操作提供了内置支持。在本菜谱中,我们将探讨如何使用它。

假设我们正在处理以下类型:

class Course(val name: String, val lecturer: Lecturer, val isPaid: Boolean = false)
class Student(val name: String, val courses: List<Course>)
class Lecturer(val name: String)

我们还有一个getStudents(): List<Student>函数,它返回数据库中所有学生的列表。

给定getStudents(): List<Student>函数,我们将实现getCoursesWithSubscribedStudents(): Map<Course, List<Student>>函数,该函数负责提取所有学生订阅的课程映射以及每个课程订阅的学生列表。

如何实现...

  1. 声明一个函数头:
fun getCoursesWithSubscribedStudents(): Map<Course, List<Student>> 
  1. 将每个学生映射到课程-学生配对的列表:
fun getCoursesWithSubscribedStudents(): Map<Course,
 List<Student>> =
    getStudents()
 .flatMap { student ->
                student.courses.map { course -> course to student }
            }
  1. 按照课程对课程-学生配对进行分组:
fun getCoursesWithSubscribedStudents(): Map<Course,
 List<Student>> =
    getStudents()
            .flatMap { student ->
                student.courses.map { course -> course to student }
            }
 .groupBy { (course, student) -> course }
  1. Pair<Course, List<Student>>类型应用映射转换:
fun getCoursesWithSubscribedStudents(): Map<Course,
 List<Student>> =
    getStudents()
            .flatMap { student ->
                student.courses.map { course -> course to student }
            }
            .groupBy { (course, _) -> course }
 .map { (course, courseStudentPairs) -> 
                course to courseStudentPairs.map { (_, student) -> 
                 student } 
            }
  1. 在最后应用一个toMap()函数:
fun getCoursesWithSubscribedStudents(): Map<Course,
 List<Student>> =
    getStudents()
            .flatMap { student ->
                student.courses.map { course -> course to student }
            }
            .groupBy { (course, _) -> course }
            .map { (course, courseStudentPairs) ->
                course to courseStudentPairs.map { (_, student) ->
                 student }
            }
 .toMap()

它是如何工作的...

我们首先使用 flatMap() 函数将学生列表转换为 Pair<Course, Student> 类型的列表。接下来,我们应用 groupBy() 函数将这些配对按不同的 Course 实例进行分组。分组操作的结果是以下类型的数据——Map.Entry<Course, List<Pair<Course, Student>>>。我们需要将 Map.Entry.value 属性的类型转换为 List<Student> 类型。我们通过以下映射转换函数实现它:

map { (course, courseStudentPairs) ->
    course to courseStudentPairs.map { (_, student) -> student }
}

因此,每个 Course 实例都与订阅它的学生列表相关联(Pair<Course, List<Student>>)。请注意,这里使用了中缀 to 函数来实例化 Pair 类型。最后,我们调用 toMap() 函数,它从课程-学生配对列表生成最终的 Map<Course, List<Students>> 实例。

还有更多...

我们还可以通过使用 mapValues 函数将我们的映射构建操作修改为更简洁的形式:

fun getCoursesWithSubscribedStudents(): Map<Course, List<Student>> =
        getStudents()
                .flatMap { student ->
                    student.courses.map { course -> course to student }
                }
                .groupBy { (course, _) -> course }
                .mapValues { (course, courseStudentPairs) ->
                    courseStudentPairs.map { it -> it.second }
                }

参见

  • 本食谱中的代码在映射操作中使用了解构类型声明。如果您想了解更多关于这个话题的信息,可以查看第二章中的 Destructuring types 食谱,Expressive Functions and Adjustable Interfaces

第五章:精美的设计模式采用 Kotlin 概念

在本章中,我们将涵盖以下食谱:

  • 实现策略模式

  • 探索委托模式的强大功能

  • 实现委托类属性

  • 使用观察者模式跟踪状态

  • 使用可撤销委托限制属性更新

  • 通过定义自定义属性委托实现高级观察者模式

  • 使用 Lazy 委托

  • 智能方式实现构建器

简介

以下章节将介绍适用于各种编程问题的流行通用设计模式。以下食谱专注于利用 Kotlin 内置的语言支持来实现特定概念和模式。除了基本设计模式,如策略或构建器之外,该章节将重点介绍在多种应用程序和场景中不同用途的委托。一旦你熟悉了本章中介绍的概念,你将能够在设计和开发优雅且可靠系统时利用语言内置的功能。

实现策略模式

策略设计模式用于提供一组可互换的策略,这些策略可以应用于给定的输入并返回特定类型的输出。我们可以将策略的概念理解为可以应用于输入的动作或算法。负责处理输入的机制应该能够在运行时在提供的策略之间切换。为了说明策略模式,我们将实现一个文本格式化机制,允许我们对输入文本进行转换并将其打印到控制台。我们将实现一个名为Printer的类,它将为打印文本到控制台提供一个printText(text: String)函数。在将文本打印到控制台之前,Printer类将根据所选的文本格式化策略对给定的text参数进行转换。

如何做到这一点...

  1. 实现Printer类:
class Printer(val textFormattingStrategy: (String) -> String) {
    fun printText(text: String) {
        val processedText = textFormattingStrategy(text)
        println(processedText)
    }
}
  1. 添加示例策略:
val lowerCaseFormattingStrategy: (String) -> String = {
    it.toLowerCase()
}

val upperCaseFormattingStrategy: (String) -> String = {
    it.toUpperCase()
}

它是如何工作的...

让我们先测试一下我们的Printer类在实际操作中的工作情况。首先,声明两个Printer类的实例——第一个实例使用lowerCaseFormattingStrategy作为textFormattingStrategy属性的值,第二个实例使用upperCaseFormattingStrategy

val lowerCasePrinter = Printer(lowerCaseFormattingStrategy)
val upperCasePrinter = Printer(upperCaseFormattingStrategy)

接下来,让我们使用它们来格式化和显示以下文本:

val text = "This functional-style Strategy pattern looks tasty!"

lowerCasePrinter.printText(text)
upperCasePrinter.printText(text)

以下输出将打印到控制台:

this functional-style strategy pattern looks tasty!
THIS FUNCTIONAL-STYLE STRATEGY PATTERN LOOKS TASTY!

Printer.textFormattingStrategy属性是一个函数,它接受一个String类型的单个参数,并返回一个String类型的输出。它在printText(text: String)函数内部使用text参数调用,并且其输出由该函数返回。

还有更多...

你可以通过实现自己的文本格式化策略来练习。尝试实现一个新的文本格式化策略,称为capitalizeFormattingStrategy,它将负责将输入文本的首字母大写。完成后,创建一个新的策略,由之前实现的两个策略——lowerCaseFormattingStrategycapitalizeFormattingStrategy——组合而成。你可以参考第三章中的函数组合配方,用 Kotlin 函数式编程特性塑造代码来了解更多关于组合函数的通用方法。

参见

  • 如果你不太熟悉用于声明Printer.textFormattingStrategy属性的更高阶函数的概念,你可以探索第三章中的使用高阶函数工作配方,用 Kotlin 函数式编程特性塑造代码

探索委托模式的强大功能

委托模式是典型类继承的一个很好的替代方案。委托允许一个类从另一个类派生或实现一个接口。然而,在底层,派生类不是基类的子类,而是使用组合来提供基类的属性给派生类。每当对基类部分的属性发出请求时,它会被重定向到委托对象。这类似于子类将请求委派给父类。然而,委托不仅允许我们实现与继承相同的代码重用性,而且它还更加强大和可定制。Kotlin 通过提供使用by关键字声明委托的内置支持,使得委托模式更加引人注目。

在这个配方中,我们将实现一个组合依赖类,模拟一个简单的图书管理系统。我们将编写一个 UML 类图的代码,该类图描述了一组使用继承的依赖类。然而,我们将使用委托模式而不是任何继承出现。

准备工作

我们将使用委托模式而不是继承来实现以下类集:

图片

在这个类图中,你可以看到两个基类,它们从BasePublication类派生,该类有BookMagazine子类,以及BaseUser类,它被MemberLibrarian子类扩展。请注意,这些基类正在实现声明其属性的相应接口。BaseUser类实现了User接口,而BasePublication类实现了Publication接口。还有一个Rental接口,它声明了由Book子类实现的方法。

为了使用语言内置的语言特性来实现委托,我们将直接操作接口并移除任何现有的继承。我们不会扩展 BaseUserBasePublication 基类,而是将它们用作最终 UserLibrarianBookMagazine 类的属性,如下面的图所示:

图片

如何做到这一点...

  1. 声明实现 Publication 接口的 Magazine 类:
class Magazine(val number: Int,
               title: String,
               pageCount: Int) : Publication
  1. Publication 接口委托给 Publication 类型的类属性:
class Magazine(val number: Int,
               val publication: Publication) :
        Publication by publication
  1. 实现 Rentable 接口:
interface Rentable {
    var currentUser: Optional<User>

    fun availableToRent() = !currentUser.isPresent

    fun doRent(user: User): Boolean {
        return if (availableToRent()) {
            currentUser = Optional.of(user)
            true
        } else {
            false
        }
    }
}
  1. 实现 Book 类,将其 Publication 接口功能委托给类成员:
class Book(val publicationDate: Instant,
           val author: String,
           val publication: Publication) :
        Publication by publication, Rentable {

    override var currentUser: Optional<User> = Optional.empty()
}
  1. 实现 MemberLibrarian 类,实现 User 接口并将其委托给它们的类属性:
class Member(val currentRentals: List<Rentable>,
             name: String,
             isActive: Boolean,
             user: User) : User by user

class Librarian(user: User) : User by user

它是如何工作的...

使用 by 关键字,我们将 UserPublication 接口的实现委托给作为类成员定义的专用对象。在 BookMagazine 类的情况下,Publication 接口的责任被委托给 publication: Publication 类属性,而在 MemberLibrarian 类的情况下,User 接口的责任被委托给 user: User 属性。

现在,让我们探索如何使用委托类型。让我们首先创建一个 Book 类的实例。我们通过重用原始的 BasePublication 类声明提供了一个 Book.publication 属性,其类型为 Publication

class BasePublication(override val title: String, 
                      override val pageCount: Int): Publication

注意,我们能够直接从 Book 类实例访问 Publication 接口的所有公共成员。对这些 Publication 接口属性的任何请求都被重定向到 Book 类的 val publication 属性:

val book = Book(Instant.now(), "Sam", 
        BasePublication("Kotlin Standard Library Cookbook",
         Integer.MAX_VALUE))

println("${book.title} written by ${book.author} has ${book.pageCount} pages.")

在结果中,前面的代码应该在控制台输出以下内容:

Kotlin Standard Library Cookbook written by Sam has 2147483647 pages.

参见

  • 另一种优秀的委托设计模式与委托类属性相关。你可以在 实现委托类属性 章节中了解更多信息。

实现委托类属性

Kotlin 中的类属性不仅仅是普通的类字段。Kotlin 属性的关键特性是它们的值由自动生成的访问器函数指定。Kotlin 中的每个类属性都有一组专用的访问器函数可用。默认情况下,Kotlin 编译器会生成一个字段来存储属性的值及其 getter 或 setter。每个不可变的val属性都有一个相应的get()函数,而用var关键字声明的可变属性除了get()函数外,还有一个set()函数。我们还可以覆盖访问器函数的默认实现,这使得属性高度可定制且功能强大。例如,我们可以覆盖属性的get()函数并提供一个自定义实现,这样就可以阻止编译器在字段中存储属性的值。此外,属性由其访问器函数表示而不是由字段值表示,这使得属性委托成为可能。属性委托的基本用例包括:

  • 实现惰性属性——仅在首次访问时计算其值的属性

  • 可观察属性——监听器会在属性发生变化时收到通知

  • 将属性存储在映射中,而不是为每个属性单独存储一个字段

在这个菜谱中,我们将学习如何创建一个函数,通过委托其属性以存储在映射中来轻松地将类实例序列化为 JSON 格式。

准备工作

类似于接口委托,类属性委托是通过以下方式使用by关键字实现的:

class MyClass {
    var property: String by MyDelegate
}

被委托的对象应实现以下接口之一——来自kotlin.properties包的ReadWritePropertyReadOnlyProperty。这些接口公开了getValue()setValue()函数,它们为属性提供值。

我们将使用Gson库将对象转换为它们的 JSON 格式表示。这是一个广泛使用的 Java 库,用于处理 JSON 格式的对象。您可以在其 GitHub 网站上了解更多关于该库的信息(github.com/google/gson)。如果您使用 Gradle 构建工具,您需要将 Gson 工件添加到项目依赖项中:

dependencies {
    implementation 'com.google.code.gson:gson:2.8.4'
}

如何实现...

  1. 实现包含Map<String, Any>类型数据属性的Client类:
data class Client(private val data: Map<String, Any>)
  1. 实现以下CreditCard类:
data class CreditCard(val holderName: String,
                      val number: String,
                      val cvcCode: String,
                      val expiration: Long)
  1. nameemailcreditCards属性添加到Client类中,并将它们委托给data属性:
data class Client(private val data: Map<String, Any>) {
    val name: String by data
    val email: String by data
    val creditCards: List<CreditCard> by data
}
  1. 实现成员函数toJson(): String,允许我们将Client类型对象序列化为 JSON 格式,以及负责相反操作的实用函数fromJson(json: String): Client
data class Client(private val data: Map<String, Any>) {
    val name: String by data
    val email: String by data
    val creditCards: List<CreditCard> by data

    /**
     * Function for serializing instance of Client class into
       JSON format
     */
    fun toJson(): String = gson.toJson(data)

    companion object {
        private val gson = Gson()

        /**
         * Utility function for instantiating Client class from
           JSON string
         */
        fun fromJson(json: String): Client {
            val mapType = object : TypeToken<Map<String,
             Any>>() {}.type
            val data: Map<String, Any> = gson.fromJson(json,
             mapType)
            return Client(data)
        }
    }
}

它是如何工作的...

类属性可以被委派给一个 MapMutableMap 实例,该实例包含 String 类型的键和 Any 类型的值。映射的键对应于类属性的名称,与它们关联的映射值存储属性值。委派给映射的映射将在委派属性更新时动态更新。

让我们看看如何利用这个菜谱中实现的 Client 类。我们可以通过将 Map 实例传递给类构造函数来实例化 Client 类:

val SAMPLE_CLIENT_MAP = mapOf("name" to "Mark Zuck",
        "email" to "mark@fb.com",
        "creditCards" to listOf(
                CreditCard("Mark Zuckerberg", "123345456789", "123",
                 1527330705017),
                CreditCard("Mark Zuckerberg", "987654321", "321",
                 1527330719816))
)
val client1 = Client(SAMPLE_CLIENT_MAP)

我们还可以使用 fromJson() 函数实例化 Client 类,传递一个包含样本 Client 类型对象 JSON 表示的字符串:

@Language("JSON")
const val SAMPLE_CLIENT_JSON =
        "{\n  \"name\": \"Mark Zuck\",
          \n  \"email\": \"mark@fb.com\",
          \n  \"creditCards\": [
          \n    {
          \n      \"holderName\": \"Mark Zuckerber\",
          \n      \"number\": \"123345456789\",
          \n      \"cvc\": \"123\",
          \n      \"expiration\": 1527330705017
          \n    },
          \n    {
          \n      \"holderName\": \"Mark Zuckerber\",
          \n      \"number\": \"987654321\",
          \n      \"cvc\": \"321\",
          \n      \"expiration\": 1527330719816
          \n    }
          \n  ]
          \n}"
val client2 = Client.fromJson(SAMPLE_CLIENT_JSON)

如果你正在使用 IntelliJ IDE,你可以使用一个酷炫的语言注入功能,该功能允许我们将另一个语言的代码片段作为字符串类型注入,并提供对特定语言语法进行编辑和格式化的支持。你可以用它来将 JSON 片段注入为 Kotlin 字符串。你可以在官方 JetBrains 教程中了解更多信息(www.jetbrains.com/help/idea/using-language-injections.html)。

在底层,Client.fromJson() 函数使用 Gson 将 JSON 数据转换为 Map<String, Any> 实例。

我们现在可以测试这两种方法,并将 client1client2 对象的内容打印到控制台:

println("name: ${client1.name}, mail: ${client1.email}, cards: ${client1.creditCards}")
println("name: ${client2.name}, email: ${client2.email}, cards: ${client2.creditCards}")

作为结果,我们将得到以下输出打印到控制台:

name: Mark Zuck, email: mark@fb.com, cards: [{holderName=Mark Zuckerber, number=123345456789, cvc=123, expiration=1.527330705017E12}, {holderName=Mark Zuckerber, number=987654321, cvc=321, expiration=1.527330719816E12}]

name: Mark Zuck, email: mark@fb.com, cards: [CreditCard(holderName=Mark Zuckerberg, number=123345456789, cvcCode=123, expiration=1527330705017), CreditCard(holderName=Mark Zuckerberg, number=987654321, cvcCode=321, expiration=1527330719816)]

在这两种情况下,无论选择哪种方式实例化 Client 类,所有类属性都存储在 data 映射对象中。将属性委派给映射允许我们实现一个机制,自动将 Client 对象的状态导出到映射中。映射对象在 Client 类中内部存储,但也可以在别处声明。

更多内容...

在这个菜谱中,我们创建了包含不可变 val 属性的 Client 类。为了存储可变的 var 属性,我们可以使用 MutableMap 实例而不是只读的 Map

内置对类属性的支持是语言的一个强大功能。它为以整洁的方式塑造代码带来了惊人的可能性。当你在一个更复杂的项目上工作时,你绝对应该尝试一下。例如,你可以将你的实体属性委派给直接从数据库读取和写入。标准库中也内置了一些现成的委托,如 LazyObservable 委托。你可以在本章的下一节中了解更多关于它们应用的信息。你可以在官方标准库文档中探索内置委托的全套:kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/index.html.

参见

  • 如果你想探索接口委托的概念,可以查看探索委托模式的强大功能配方

  • 你还应该了解标准库提供的标准属性委托

使用观察者模式跟踪状态

观察者模式是一个概念,其中一个对象允许我们订阅其状态的变化,并在对象状态发生变化时自动通知一组观察者。借助标准库提供的内置Observable属性委托,在 Kotlin 中实现观察者模式非常简单。在这个配方中,我们将实现一个可观察变量,允许我们订阅其状态的变化。订阅的监听器应在任何状态更新后立即被通知。在以下示例中,我们将声明temperature: Int变量并订阅其变化。

如何做...

  1. 定义温度变量的初始值:
val initialValue = 1
  1. 声明将被观察的变量的监听器:
val initialValue = 1
val changesListener: (KProperty<*>, Int, Int) -> Unit =
 { _, _: Int, newValue: Int -> println("Current temperature: $newValue") }
  1. 声明temperature变量,将其值委托给由Delegates.observable()函数返回的ReadWriteProperty实例:
val initialValue = 1
val changesListener: (KProperty<*>, Int, Int) -> Unit =
        { _, _: Int, newValue: Int -> println("Current temperature: $newValue") }
var temperature: Int by Delegates.observable(initialValue, changesListener)

它是如何工作的...

我们将var temperature变量委托给Delegates.observable()函数的结果,该函数返回一个ReadWriteProperty类的实例。这一事实使得我们可以将temperature声明为一个可变变量。observe()函数接受两个参数——初始值和一个钩子函数的实例,该函数将在对委托变量进行每次更改时被调用。在我们的例子中,我们将该函数实例化为 lambda 块,其目的是将新的temperature值打印到控制台。

让我们测试我们的实现将如何工作。我们将直接修改温度值几次:

temperature = 10
temperature = 11
temperature = 12
temperature = 30

因此,我们得到以下输出:

Current temperature: 10
Current temperature: 11
Current temperature: 12
Current temperature: 30

在温度值每次更改时,监听器函数都会被调用,并将属性的前一个和新的值作为参数传递。

相关内容

  • 如果你想探索属性委托在底层是如何工作的,可以查看实现委托类属性配方

使用可撤销委托限制属性更新

在这个配方中,我们将探索标准库提供的可撤销委托的使用。与 Observable 类似,Vetoable 跟踪对委托属性的更改。然而,如果未满足预定义的更新条件,可撤销委托可以拒绝更新委托属性。我们将声明一个Int类型的变量并指定更新条件,这样我们只能在变化的绝对值大于或等于10时更新变量。

如何做...

  1. 让我们从为温度变量定义一个初始值开始:
val initialValue = 1
  1. 定义观察变量的更新条件:
val initialTemperature = 1
val updateCondition: (KProperty<*>, Int, Int) -> Boolean =
 { _, oldValue: Int, newValue: Int -> Math.abs(oldValue - newValue) >= 10 }
  1. 声明 temperature: Int 变量并将其委派给 Delegates.vetoable() 函数的结果:
val initialTemperature = 1
val updateCondition: (KProperty<*>, Int, Int) -> Boolean =
        { _, oldValue: Int, newValue: Int -> Math.abs(oldValue - newValue) >= 10 }
var temperature: Int by Delegates.vetoable(initialTemperature, updateCondition)

它是如何工作的...

我们将 var temperature 变量委派给 Delegates.vetoable() 函数的结果,该函数返回一个 ReadWriteProperty 类的实例。这一事实使得将 temperature 声明为可变变量成为可能。vetoable() 函数接受两个参数——初始值和将被调用的钩子函数的实例,该函数将在对委派变量进行的每次更改上被调用。

该函数提供了委派变量的当前值和新值的候选者。作为结果,该函数返回一个布尔值——如果值可以更新,则返回 true,如果更新条件不满足,则返回 false。在我们的情况下,我们将该函数实例化为 lambda 块,在其中检查变化的绝对值是否大于或等于 10

{ _, oldValue: Int, newValue: Int -> Math.abs(oldValue - newValue) >= 10 }

让我们测试我们的实现将如何工作。我们将直接修改 temperature 的值几次,使用不同的值,并通过将 temperature 状态打印到控制台来验证更新是否被批准:

temperature = 10
println("Current temperature: $temperature")

temperature = 11
println("Current temperature: $temperature")

temperature = 12
println("Current temperature: $temperature")

temperature = 30
println("Current temperature: $temperature")

因此,我们得到以下输出打印出来:

Current temperature: 1
Current temperature: 11
Current temperature: 11
Current temperature: 30

如您所见,每次我们用不满足指定条件的值赋值给温度时,温度的值都保持不变。

参见

  • 在下一个菜谱中,使用自定义属性代理实现高级观察者,我们将通过实现我们的自定义代理来结合 Observable 和 Vetoable 代理的功能。继续阅读,了解如何在一个属性代理中同时过滤属性更新并实现观察者模式。

通过定义自定义属性代理实现高级观察者模式

在这个菜谱中,我们将实现一个自定义的、通用的属性代理,结合标准库中可用的 Observable 和 Vetoable 代理的功能。换句话说,我们希望实现一个属性代理,允许我们通知订阅的监听器关于观察属性所做的任何更改。同时,我们还想让代理允许过滤对委派属性的更新。在这个例子中,我们将声明 temperature: Int 变量,并将其委派给我们的 ObservableVetoable 代理类的自定义实现。我们将创建一个通用类,允许我们传递初始值、负责过滤属性更新的函数以及将在属性更改后立即调用的函数。

如何实现...

  1. 定义一个名为 ObservableVetoableDelegate 的自定义属性代理,作为 ObservableProperty 类的子类:
class ObservableVetoable<T>(initialValue: T,
                          val updatePrecondition: (old: T, new: T)
                           -> Boolean,
                          val updateListener: (old: T, new: T)
                           -> Unit) :
        ObservableProperty<T>(initialValue = initialValue) {

    override fun beforeChange(property: KProperty<*>,
                              oldValue: T,
                              newValue: T): Boolean =
            updatePrecondition(oldValue, newValue)

    override fun afterChange(property: KProperty<*>,
                             oldValue: T,
                             newValue: T) = 
            updateListener(oldValue, newValue)
}
  1. 定义 ObservableVetoable 类所需的 initialTemperatureupdatePreconditionupdateListener 参数:
val initialTemperature = 1
val updatePrecondition: (Int, Int) -> Boolean =
        { oldValue, newValue -> Math.abs(oldValue - newValue) >= 10 }

val updateListener: (Int, Int) -> Unit = { _, newValue -> println(newValue) }
  1. 通过委托给ObservableVetoable类实例来声明temperature: Int变量:
var temperature: Int by ObservableVetoable(initialTemperature, 
                                           updatePrecondition, 
                                           updateListener)

它是如何工作的...

我们已经定义了ObservableVetoable类,并将var temperature: Int变量委托给ObservableVetoable实例。我们的ObservableVetoable类扩展了ObservableProperty类,后者在底层实现了ReadWriteProperty接口。正因为如此,ObservableProperty允许我们将可变属性委托给它。ObservableProperty类还具有beforeChange(): BooleanafterChange(): Unit公开函数,这些函数在setValue()函数内部被调用:

public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
    val oldValue = this.value
    if (!beforeChange(property, oldValue, value)) {
        return
    }
    this.value = value
    afterChange(property, oldValue, value)
}

如您所见,每当委托属性被分配给新值时,beforeChange()函数就会被调用以检查新值是否符合指定条件。如果条件满足,属性将被更新,并且会调用afterChange()函数。实际上,我们的ObservableVetoable类接受函数实现的实例,updatePreconditionupdateListener,这些函数覆盖了beforeChange()afterChange()基函数。这样,我们既能观察委托属性的变化,又能立即通知变化监听器以过滤对其所做的更改。

例如,我们可以通过用不同的值更新temperature变量五次来测试我们的实现:

temperature = 11
temperature = 12
temperature = 13
temperature = 14
temperature = 30

结果,我们将在控制台输出只有两行:

11
30

这意味着我们的机制正在正常工作,因为我们的更新前提条件函数正在检查变化的绝对值是否大于或等于10。只有当新值被updatePrecondition()函数接受时,才会调用updateListener()

参见

  • 如果你想熟悉 Kotlin 中属性委托支持的基础知识,请查看实现委托类属性配方,其中包含对语言对委托概念的深入介绍和解释。

  • 你还可以探索使用 Vetoable 委托限制属性更新使用观察者模式跟踪状态配方,以熟悉标准库提供的观察者和 Vetoable 委托。

使用懒委托

懒初始化是另一种有专用委托实现的设计模式,该实现包含在标准库中。懒初始化的概念指的是延迟创建对象、计算值或执行某些昂贵操作,直到第一次需要时。在这个配方中,我们将定义一个示例类,CoffeeMaker,并通过Lazy委托声明其类型的对象。然后我们将对该对象执行示例操作,以探索懒委托在实际操作中的工作方式。

如何做到这一点...

  1. 让我们从定义CoffeeMaker类开始:
class CoffeeMaker {
    init {
        println("I'm being created right now... Ready to make some
         coffee!")
    }

    fun makeEspresso() {
        println("Un espresso, per favore!")
    }

    fun makeAmericano() {
        print("Un caffè americano, per favore!")
    }
}
  1. 使用lazy委托声明一个CoffeMaker类型的变量:
val coffeeMaker: CoffeeMaker by lazy { CoffeeMaker() }

它是如何工作的...

让我们通过运行以下代码来测试 coffeeMaker 实例的行为:

val coffeMaker: CoffeeMaker by lazy { CoffeeMaker() }
println("Is the CoffeMaker created already?")

coffeMaker.makeEspresso()
coffeMaker.makeAmericano()

下面是打印到控制台的结果:

Is the CoffeMaker created already?
I'm being created right now... Ready to make some coffe!
Un espresso, per favore!
Un caffè americano, per favore!

如您所想象的那样,CoffeeMaker 类的构造函数仅在第一次请求 coffeeMaker 变量时被调用。实际上,传递给懒函数的 lambda 块是在调用 coffeeMaker.makeEspresso() 函数时被调用的。一旦 CoffeeMaker 对象被实例化,它将被用于对该对象进行的任何连续操作。

默认情况下,懒属性的评估是同步的;值仅在单个线程中计算,所有线程都将看到相同的值。如果不需要初始化代理的同步,以便多个线程可以同时执行它,请将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。如果您确信初始化总是在单个线程上发生,您可以使用 LazyThreadSafetyMode.NONE 模式,它不提供任何线程安全保证,也没有相关的开销(kotlinlang.org/docs/reference/delegated-properties.html#lazy)。

lazy() 函数创建并返回 Lazy<T> 接口的实例:

public interface Lazy<out T> {
    public val value: T
    public fun isInitialized(): Boolean
}

如您所见,value 属性是不可变的,并且无法使用懒代理声明可变变量或属性。在底层,Lazy 实现返回它所持有的对象的特定值,并内部检查它是否已经被评估。在第一次访问对象时,传递给懒函数作为参数的 initializer 函数正在执行,并且其结果被分配给专用属性。之后,将使用缓存的值而不是每次重新评估值。

参考也

  • 如果您想探索属性代理在底层是如何工作的,请查看 实现委托类属性 菜单

智能地实现构建器

建造者设计模式是 Java 语言中用于实例化复杂类型最常用的机制之一。它在 Joshua Bloch 的《Effective Java》一书中被强烈推荐。Bloch 表示,当我们需要实现多个构造函数时,应该使用构建器。他还提到,构建器模式模拟了命名可选参数。然而,在 Kotlin 中,那些实现专用构建器类的论据不再有效。Kotlin 允许我们为类构造函数参数和属性提供默认值,并且它具有内置的命名参数支持。鉴于这些 Kotlin 特性,在大多数情况下,我们无需实现构建器,因为我们可以直接使用语言内置的概念来实现其功能。然而,在 Kotlin 中,我们可以将构建器模式适应得更加灵活。我们将利用构建器的概念,结合高阶函数和内联 lambda 参数的可能性,为给定类的实例化定义类似 DSL 的语法。

准备工作

假设我们有一个 Dialog 类在外部依赖中指定,提供的接口如下:

class Dialog {
    lateinit var title: String
    lateinit var message: String
    lateinit var messageColor: String
    lateinit var image: ByteArray

    fun show() = println("Dialog...\n$this")

    override fun toString() = "Title: $title \nImage: $image \nMessage:
     $message"
}

Dialog 类公开以下属性——title: Stringmessage: StringmessageColor: Stringimage: File。我们将实现一个 DialogBuilder 类,它允许我们使用构建器模式实例化 Dialog 类。作为结果,我们希望创建一个机制,允许我们使用类似于 JSON 格式的 DSL 语法实例化 Dialog 类型:

val dialog: Dialog = 
    dialog {
        title {
            "Title"
        }
        message {
            text = "Message"
            color = "#FF0000"
        }
        image {
            File("path")
        }
    }

如何做到这一点...

  1. 创建一个包含 Dialog 类属性所需值的 DialogBuilder 类:
class DialogBuilder() {
 private var titleHolder = "-"
  private var messageHolder = StyleableText("-", "#000")
 private var imageHolder: File = File.createTempFile("empty", "")

  class StyleableText(
 var text: String = "",
      var color: String = "#000"
  )
}
  1. 添加 title()message()image() 函数,允许我们修改 titleHoldermessageimage 属性:
class DialogBuilder() {
  private var titleHolder = "-"
  private var messageHolder = StyleableText("-", "#000")
  private var imageHolder: File = File.createTempFile("empty", "")

  fun title(block: () -> String) {
 titleHolder = block()
 }

 fun message(block: StyleableText.() -> Unit) {
 messageHolder.apply { block() }
  }

 fun image(block: File.() -> Unit) {
 imageHolder.apply { block() }
  }

  class StyleableText(
      var text: String = "",
      var color: String = "#000"
  )
}
  1. 添加 build() 函数,返回 Dialog 类实例:
class DialogBuilder() {
  private var titleHolder = "-"
  private var messageHolder = StyleableText("-", "#000")
  private var imageHolder: File = File.createTempFile("empty", "")

  fun title(block: () -> String) {
      titleHolder = block()
  }

  fun message(block: StyleableText.() -> Unit) {
      messageHolder.apply { block() }
  }

  fun image(block: File.() -> Unit) {
      imageHolder.apply { block() }
  }

  fun build(): Dialog = Dialog().apply {
      title = titleHolder
      message = messageHolder.text
      messageColor = messageHolder.color

      imageHolder.apply {
          image = readBytes()
 } }

  class StyleableText(
      var text: String = "",
      var color: String = "#000"
  )
}
  1. 声明一个构造函数,它接受一个负责初始化 DialogBuilder 类的函数:
class DialogBuilder() {
  private var titleHolder = "-"
  private var messageHolder = StyleableText("-", "#000")
  private var imageHolder: File = File.createTempFile("empty", "")

  constructor(initBlock: DialogBuilder.() -> Unit): this() {
 initBlock()
 }

  fun title(block: () -> String) {
      titleHolder = block()
  }

  fun message(block: StyleableText.() -> Unit) {
      messageHolder.apply { block() }
  }

  fun image(block: File.() -> Unit) {
      imageHolder.apply { block() }
  }

  fun build(): Dialog = Dialog().apply {
      title = titleHolder
      message = messageHolder.text
      messageColor = messageHolder.color

      imageHolder.apply {
          image = readBytes()
      }
  }

  class StyleableText(
      var text: String = "",
      var color: String = "#000"
  )
}
  1. 实现一个 dialog() 辅助函数,它接受一个负责初始化 DialogBuilder 的函数,并返回 Dialog 类实例:
fun dialog(block: DialogBuilder.() -> Unit): Dialog =            DialogBuilder(block).build()

它是如何工作的...

让我们先测试一下如何在实际操作中使用我们的 dialog() 函数。让我们用它来定义一个示例 Dialog 类实例:

val dialog =
        dialog {
            title {
                "Warning!"
            }
            message {
                text = "You have 99999 viruses on your computer!"
                color = "#FF0000"
            }
            image {
                File.createTempFile("red_alert", "png")
            }
        }

现在,我们可以在 dialog 变量上调用 show() 函数,这将打印以下输出到控制台:

Dialog...
Title: Warning! 
Image: [B@548c4f57 
Message: You have 99999 viruses on your computer!

这非常酷!DialogBuilder 类允许我们以可读和自然的方式组合 Dialog 类型的实例。

通过使用高阶函数和 lambda 参数的内联表示法,实现了 Dialog 类组合的新语法。请注意,每个准备目标类属性状态信息的 DialogBuilder 函数,如 title()message()image(),都接受一个单一的功能参数。功能参数以 lambda 块的形式传递。在构建方法中作为参数使用的有两种类型的函数类型——第一种简单地返回属性的特定值,第二种返回带有接收者类型的函数。第二种类型的函数返回 Unit 但接受接收者类型的一个实例。

函数类型允许有一个额外的接收者类型,该类型在点号之前声明。在以下表示法中,A.(B) -> C 类型代表一个可以在 A 类型接收者对象上调用,带有 B 类型参数并返回 C 类型值的函数。在函数字面量的主体内部,传递给调用的接收者对象成为隐式的 this,这样你就可以无需任何额外的限定符访问该接收者对象的成员,或者使用 this 关键字访问接收者对象。你可以在官方 Kotlin 参考kotlinlang.org/docs/reference/lambdas.html#function-types中了解更多关于可用函数类型及其应用的信息。

例如,title(block: () -> String) 函数简单地调用块函数并将结果分配给 DialogBuilder.titleHolder 属性。另一方面,当我们处理复杂类型,如 StyleableText 时,我们使用带有接收者类型函数参数的第二种方法。例如,让我们分析 message(block: StyleableText.() -> Unit) 函数:

fun message(block: StyleableText.() -> Unit) {
    messageHolder.apply { block() }
}

在底层,它执行的是 block: StyleableText.() -> Unit 参数来直接修改 messageHolder: StyleableText 属性实例。block 参数通过 apply 函数内部的 () 修饰符被调用,在这种情况下,它通过局部 this 关键字提供了对 messageHolder 实例的访问。同样的方法也用于 DialogBuilder 类的构造函数中:

constructor(initBlock: DialogBuilder.() -> Unit): this() {
    initBlock()
}

DialogBuilder 类的接收者被提供给功能参数,并且作为 initBlock 传递的函数在构造函数内部被调用,这允许我们修改其状态。

还有更多...

DSL 风格构建器的概念在许多 Kotlin 库和框架中被广泛使用。它也被标准库所采用。例如,我们可以使用 kotlinx.html 库中的 html 函数(github.com/Kotlin/kotlinx.html)来生成 HTML 代码:

val result =
        html {
            head {
                title { +"HTML encoding with Kotlin" }
            }
            body {
                h1 { "HTML encoding with Kotlin" }
                p { +"this format can be used as an alternative to HTML" }

                // an element with attributes and text content
                a(href = "http://jetbrains.com/kotlin") { +"Kotlin" }
            }
        }
println(result)

上述代码将生成有效的 HTML 代码并将其打印到控制台:

<html>   
    <head>     
        <title>HTML encoding with Kotlin</title>   
    </head>   
    <body>     
        <h1>HTML encoding with Kotlin</h1>     
        <p>this format can be used as an alternative to HTML</p>     
        <a href="http://jetbrains.com/kotlin">Kotlin</a>   
    </body>
</html>

你可以在kotlinlang.org/docs/reference/type-safe-builders.html.探索 Kotlin 中 Builders 的更多精彩应用。

参见

  • 如果你想了解更多关于高阶函数和函数参数内联表示法的技术细节,你可以研究第二章中的“闭包类型参数的内联”配方,表达性函数和可调整接口,以及第三章中的“使用高阶函数进行工作”配方,使用 Kotlin 函数式编程特性塑造代码

第六章:友好的 I/O 操作

在本章中,我们将介绍以下食谱:

  • 读取文件内容

  • 使用use函数确保流关闭

  • 逐行读取文件内容

  • 将内容写入文件

  • 追加文件

  • 简单的文件复制

  • 遍历目录中的文件

简介

本章重点介绍 Kotlin 在 JVM 上使用FileInputStreamOutputStream类型的方法。我们将探索由标准库在kotlin.io包下提供的扩展函数组,这些函数专注于增强对 I/O 操作的支持。请注意,目前,在 Kotlin 版本 1.2 中,以下食谱仅适用于针对 JVM 平台的目标代码。

读取文件内容

在本食谱中,我们将检索文件内容作为文本并将其打印到控制台。我们将使用标准库的File.readText()扩展函数,它返回一个表示给定File实例文本内容的String

准备工作

确保你的项目中包含一个非空样本文件以读取其内容。你可以克隆书中 GitHub 仓库提供的样本项目:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook。在本食谱中,我们将使用位于样本项目src/main/resources目录中的file1.txt文件。

如何做到这一点...

  1. 导入File.separator常量并将其分配一个别名:
import java.io.File.separator as SEPARATOR
  1. 声明一个存储将要读取的文件路径的String
val filePahtName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file1.txt" 
  1. 使用指定的路径实例化一个File
val filePahtName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file1.txt"
val file = File(filePahtName)
  1. 从文件中读取文本并将其打印到控制台:
val filePahtName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file1.txt"
val file = File(filePahtName)
val fileText: String = file.readText()
println(fileText)

它是如何工作的...

readText()扩展函数返回表示给定文件文本的String值。这是一个方便读取文件内容的方法,因为它封装了从FileInputStream类读取字节的底层逻辑。在底层,在读取文件字节之前,该函数会检查文件是否有适当的大小以存储在内存中。

请记住,如果文件太大,将抛出OutOfMemoryError。每当文件太大而无法一次性处理时,你应该使用BufferedReader来访问其内容。你可以通过调用File.bufferedReader()扩展函数轻松地获得BufferedReader实例。

readText()函数还可以接受charset: Charset参数,默认设置为Charsets.UTF_8值。如果你想使用另一个charset,你可以通过传递适当的参数作为charset来指定它。你可以在kotlin.text.Charsets对象中找到可用的 charset 类型。你还可以在官方文档中找到它们:kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/-charsets

您可能已经注意到我们正在使用File.separator常量而不是硬编码的"/"字符。多亏了这一点,我们可以确保在不同的平台上使用正确的目录分隔符。为了简洁起见,您可以将File.separator导入为别名,例如import java.io.File.separator as separator

参见

  • 您还可以查看逐行读取文件内容配方,该配方解释了如何有效地逐行读取文件文本内容。

使用 use 函数确保流关闭

每当我们通过FileInputStreamFileOutputStream访问File的内容时,我们应该记住在完成文件操作后关闭它们。未关闭的流可能导致内存泄漏和性能显著下降。在这个配方中,我们将探讨如何使用标准库中kotlin.io包提供的use()扩展函数来自动关闭流。

准备工作

确保您的项目中包含一个非空样本文件以读取其内容。您可以通过克隆书中 GitHub 存储库提供的样本项目:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook。在这个配方中,我们将使用位于样本项目src/main/resources目录中的file1.txt文件。

如何做到这一点...

  1. 导入File.separator常量并将其分配别名:
import java.io.File.separator as SEPARATOR
  1. 声明一个存储将要读取的文件路径的String
val filePahtName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file1.txt" 
  1. file1.txt文件实例化FileInputStream
val filePahtName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file1.txt"
val stream = File(filePahtName).inputStream()
  1. use()函数内部从流中读取字节:
val fileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file1.txt"
val stream = File(fileName).inputStream()
stream.use {
    it.readBytes().also { println(String(it)) }
}

它是如何工作的...

首先,我们使用File.inputStream()扩展函数创建FileInputStream实例。接下来,我们在我们的流实例上调用use()扩展函数,将包含我们想要在流上执行的操作的 lambda 块作为参数。

在底层,在调用 lambda 表达式之后,use()函数会在流变量上调用close()函数。我们可以检查,当我们再次尝试使用stream变量访问文件时,将会抛出一个java.io.IOException: Stream Closed异常。

use()函数扩展了实现Closeable接口的任何类型。它接受一个 lambda 块作为参数,将可关闭资源实例作为参数传递给 lambda。use函数返回 lambda 块返回的值。在底层,使用了一个try…catch块,其中在finally块中调用了资源的close()函数。

逐行读取文件内容

在这个配方中,我们将检索文件内容作为一系列连续的文本行。我们将使用标准库扩展函数File.readLines()来返回一个表示给定File实例后续行的String类型的List

准备工作

确保你的项目中包含一个非空样本文件以读取其内容。你可以在 GitHub 仓库中找到本书提供的样本项目:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook。在这个菜谱中,我们将使用样本项目中位于src/main/resources目录下的file1.txt文件。

如何做...

  1. 导入File.separator常量并为其指定别名:
import java.io.File.separator as SEPARATOR
  1. 声明一个String,用于存储我们将要读取的文件的路径:
val filePahtName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file1.txt" 
  1. 使用指定的路径实例化一个File
val filePahtName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file1.txt"
val file = File(filePahtName)
  1. 从文件中读取文本并将其打印到控制台:
val filePathName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file1.txt"
val file = File(fileName)
val fileLines = file.readLines()
fileLines.forEach { println(it) }

工作原理...

readLines()扩展函数返回一个List<String>实例,表示给定文件的文本行。这是一种方便读取文件内容的方法,因为它封装了从FileInputStream类读取字节的底层逻辑。

请记住,如果文件太大,将抛出OutOfMemoryError。每当文件太大而无法一次性处理时,你应该使用BufferedReader来访问其内容。你可以通过调用File.bufferedReader()扩展函数轻松地获得BufferedReader实例。

readLines()函数还可以接受charset: Charset参数,默认设置为Charsets.UTF_8值。如果你想使用其他字符集,你可以通过传递适当的参数作为charset来指定它。你可以在kotlin.text.Charsets对象中找到可用的字符集类型。你还可以在官方文档中找到它们:kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/-charsets

你可能已经注意到我们使用的是File.separator常量而不是硬编码的"/"字符。多亏了这一点,我们可以确保在不同的平台上使用正确的目录分隔符。为了简洁起见,你可以使用别名导入File.separator,例如import java.io.File.separator as separator

参考内容

  • 你还可以查看读取文件内容菜谱,它解释了如何一次性检索文件的文本内容作为String

将内容写入文件

在这个菜谱中,我们将学习如何轻松创建一个新的File并向其写入文本。我们将使用标准库提供的File.writeText()扩展函数。然后,我们将通过将其打印到控制台来验证文件是否已成功创建以及是否包含正确的内容。

如何做...

  1. 导入File.separator常量并为其指定别名:
import java.io.File.separator as SEPARATOR
  1. 指定我们将要创建的新文件的路径:
val fileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}temp_file"
  1. 使用指定的文件路径实例化文件:
val fileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}temp_file"
val file = File(fileName)
  1. apply块中使用writeText()函数将文本写入文件:
val fileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}temp_file"
val file = File(fileName)
file.apply {
    val text =
 "\"No one in the brief history of computing " +
 "has ever written a piece of perfect software. " +
 "It's unlikely that you'll be the first.\" - Andy Hunt" 
 writeText(text) 
}
  1. temp_file的内容打印到控制台:
val fileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}temp_file"
val file = File(fileName)
file.apply {
    val text =
        "\"No one in the brief history of computing " +
          "has ever written a piece of perfect software. " +
          "It's unlikely that you'll be the first.\" - Andy Hunt"
    writeText(text)
}
file.readText().apply { println(this) }

它是如何工作的...

执行前面的代码后,将在src/main/resources目录下创建一个新的temp_file文件。请注意,如果temp_file已存在,它将被覆盖。接下来,使用writeText()函数,其内容将被打印到控制台:

"No one in the brief history of computing has ever written a piece of perfect software. It's unlikely that you'll be the first." - Andy Hunt

writeText()函数封装了java.io.FileOutputStream API,提供了一种优雅的方式将内容写入文件。在底层,它访问use()函数中的FileOutputStream,因此您可以确信它会在写入操作期间自动关闭任何打开的流。

如果您要写入文件中的文本太大,无法一次性处理,您可以使用BufferedWriter API 来允许您写入和追加文件。您可以使用File.bufferedWriter()扩展函数轻松地获取BufferedWriter的实例。

您还可以向writeText()函数传递额外的charset: Charset参数,默认值为Charsets.UTF_8。如果您想使用其他字符集,可以通过传递适当的参数作为charset来指定它。您可以在kotlin.text.Charsets对象中找到可用的字符集类型。您也可以在官方文档中找到它们,链接为kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/-charsets

相关内容

  • 查看关于追加文件的食谱,了解如何通过追加内容以灵活的方式修改文件内容

追加文件

在这个食谱中,我们将学习如何通过使用标准库提供的File.appendText()扩展函数轻松地创建一个新的File并写入文本。我们将通过多次追加其内容来验证文件是否成功创建以及是否包含正确的内容,并将其打印到控制台。

如何做到这一点...

  1. 导入File.separator常量并将其分配一个别名:
import java.io.File.separator as SEPARATOR
  1. 指定将要创建的新文件的路径:
val fileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}temp_file"
  1. 使用指定的文件路径实例化文件:
val fileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}temp_file"
val file = File(fileName)
  1. 如果文件已存在,则删除该文件:
val fileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}temp_file"
val file = File(fileName)
if (file.exists()) file.delete()
  1. 使用下一个字符串值追加文件:
val fileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}temp_file"
val file = File(fileName)
if (file.exists()) file.delete()

file.apply {
    appendText("\"A language that doesn't affect the way you think ")
 appendText("about programming ")
 appendText("is worth knowing.\"")
 appendText("\n")
 appendBytes("Alan Perlis".toByteArray())
}
  1. 将文件内容打印到控制台:
val fileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}temp_file"
val file = File(fileName)
if (file.exists()) file.delete()

file.apply {
    appendText("\"A language that doesn’t affect the way you think ")
    appendText("about programming ")
    appendText("is worth knowing.\"")
    appendText("\n")
    appendBytes("Alan Perlis".toByteArray())
}

file.readText().let { println(it) }

它是如何工作的...

执行前面的代码后,将在src/main/resources目录下创建一个新的temp_file文件,并且其内容将被打印到控制台:

"A language that doesn’t affect the way you think about programming is worth knowing."
Alan Perlis

appendText()appendBytes()函数封装了java.io.FileOutputStream API,提供了一种优雅的方式将内容追加到文件中。在底层,它们在use()函数中访问FileOutputStream,因此您可以确信它会在写入操作期间自动关闭任何打开的流。

如果你想写入文件中的文本太大,一次无法处理,你可以使用BufferedWriter API,它允许你写入和追加文件。你可以通过使用File.bufferedWriter()扩展函数轻松地获得BufferedWriter实例。

你还可以将额外的charset: Charset参数传递给appendText()函数,默认值等于Charsets.UTF_8。如果你想使用其他字符集,你可以通过传递适当的参数作为charset参数来指定它。你可以在kotlin.text.Charsets对象中找到可用的字符集类型。你还可以在官方文档中找到它们,网址为kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/-charsets

简单的文件复制

在这个菜谱中,我们将探索一种将文件内容复制到新文件中的巧妙方法。我们将从指定的路径获取一个样本File实例,并将其内容复制到新文件中。最后,我们将打印两个文件的内容到控制台以验证操作。

准备工作

确保你的项目中包含一个非空样本文件以读取其内容。你可以在 GitHub 仓库中克隆本书提供的样本项目:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook。在这个菜谱中,我们将使用位于样本项目src/main/resources目录中的file2.txt文件。

如何操作...

  1. 导入File.separator常量并为其指定别名:
import java.io.File.separator as SEPARATOR
  1. 为指定的file2.txt路径实例化一个File对象:
val sourceFileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file2.txt"
val sourceFile = File(sourceFileName)
  1. 创建一个名为file2_copy.txt的新File
val sourceFileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file2.txt"
val sourceFile = File(sourceFileName)

val targetFileName =         "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file2_copy.txt"
val targetFile = File(targetFileName)
  1. 如果file2_copy.txt存在,则删除它:
val sourceFileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file2.txt"
val sourceFile = File(sourceFileName)

val targetFileName =     "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file2_copy.txt"
val targetFile = File(targetFileName)

if (targetFile.exists()) targetFile.delete()
  1. file2.txt的内容复制到file2_copy.txt文件中:
val sourceFileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file2.txt"
val sourceFile = File(sourceFileName)

val targetFileName =     "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file2_copy.txt"
val targetFile = File(targetFileName)

if (targetFile.exists()) targetFile.delete()

sourceFile.copyTo(targetFile)
  1. 将两个文件打印到控制台以进行验证:
val sourceFileName = "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file2.txt"
val sourceFile = File(sourceFileName)

val targetFileName =     "src${SEPARATOR}main${SEPARATOR}resources${SEPARATOR}file2_copy.txt"
val targetFile = File(targetFileName)

if (targetFile.exists()) targetFile.delete()

sourceFile.copyTo(targetFile)

File(sourceFileName).readText().apply { println(this) }
File(targetFileName).readText().apply { println(this) }

它是如何工作的...

你可以运行样本代码以验证,在调用copyTo()扩展函数后,两个文件包含相同的内容。在我们的例子中,我们得到以下输出:

"Testing can show the presence of errors, but not their absence." - E. W. Dijkstra
"Testing can show the presence of errors, but not their absence." - E. W. Dijkstra

在幕后,copyTo()函数读取源文件中的InputStream到缓冲区,并将其写入目标文件的OutputStream。内部,流在use()函数块中被访问,操作完成后会自动关闭它们。

除了目标File实例外,copyTo()函数还接受两个可选参数——overwrite: Boolean,默认设置为false,以及bufferSize: Int,默认值。请注意,无论目标文件路径上的哪些目录缺失,它们都将被创建。此外,如果目标文件已存在,copyTo()函数将失败,除非覆盖参数设置为true

  • overwrite参数设置为truetarget指向一个目录时,只有当它为空时才会替换。

  • 如果在一个指向目录的 File 实例上调用 copyTo(),它将不带内容进行复制。在目标路径下只会创建一个空目录。

  • copyTo() 函数不会保留复制的文件属性,也就是说,不会保留创建/修改日期和权限。

遍历目录中的文件

在这个菜谱中,我们将探讨如何遍历给定目录中的文件。我们将从指向目录的给定 File 获取 FileTreeWalk 类实例。我们将遍历给定目录内的所有文件,包括任何嵌套的子目录。我们还将过滤文件,排除那些没有 .txt 扩展名的文件,并将它们的路径和内容打印到控制台。

准备工作

确保你的项目中包含了具有 .txt 扩展名的样本文件。您可以从 GitHub 仓库中克隆本书提供的样本项目:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook。在这个菜谱中,我们将使用样本项目的 src/main/resources 目录及其内容。

如何操作...

  1. 导入 File.separator 常量并将其分配一个别名:
import java.io.File.separator as SEPARATOR
  1. 从指向 src/main/resources 目录的 File 获取 FileTreeWalk 实例:
val directoryPath = "src${SEPARATOR}main${SEPARATOR}resources"
val fileTreeWalk: FileTreeWalk = File(directoryPath).walk()
  1. 遍历所有非空 .txt 文件并打印:
val directoryPath = "src${SEPARATOR}main${SEPARATOR}resources"

val fileTreeWalk: FileTreeWalk = File(directoryPath).walk()
fileTreeWalk
 .filter { it.isFile }
        .filter { it.extension == "txt" }
        .filter { it.readBytes().isNotEmpty() }
        .forEach {
            it.apply {
                println(path)
 println(readText())
 println()
 }
        }

工作原理...

我们首先实例化一个引用 src/main/resources 目录的 File 类型实例,并在其上调用 walk() 函数。walk() 返回 FileTreeWalk 实例,这是一个在文件系统之上的高级抽象层,允许我们遍历原始 File 对象的文件和子目录。

FileTreeWalk 扩展了 Sequence<File> 接口,并提供了 Iterator<File> 实现,这允许我们遍历文件并对它们应用任何转换操作,就像我们在处理集合时做的那样。

接下来,我们应用一些过滤操作——移除引用目录的 File 对象,移除不包含 .txt 扩展名的文件,以及从序列中移除任何空文件。最后,我们使用 forEach() 函数一起打印连续文件的路径及其内容。

如您所观察到的,FileTreeWalk 序列提供的默认顺序是从上到下。我们可以通过调用 walk() 函数并设置 direction 参数为 FileWalkDirection.BOTTOM_UP 来定义一个反向序列。

walk() 函数也有两种现成的专用变体可用——File.walkTopDown()File.walkBottomUp()。前者返回一个 FileTreeWalk 实例,其方向属性设置为 FileWalkDirection.TOP_DOWN,而后者将 direction 设置为 FileWalkDirection.BOTTOM_UP

第七章:让异步编程再次伟大

在本章中,我们将涵盖以下食谱:

  • 使用线程在后台执行任务

  • 后台线程同步

  • 使用协程进行异步、并发任务执行

  • 使用协程执行异步、并发任务执行并处理结果

  • 应用协程进行异步数据处理

  • 简单的协程取消

  • 使用 Retrofit 和协程适配器构建 REST API 客户端

  • 使用协程包装第三方回调式 API

简介

本章将讨论异步编程问题的各个方面。前两个食谱,“使用线程在后台执行任务”和“后台线程同步”,将解释标准库对使用 JVM 线程运行后台任务的支持。

在后续的食谱中,我们将更深入地探讨强大的 Kotlin 协程框架。这些食谱将解释协程在异步和并发任务执行中的通用用法。它们还将展示如何使用协程解决更具体的日常编程问题,例如并发数据处理、异步 REST 调用处理,以及以整洁的方式与第三方回调式 API 一起工作。阅读完这一章后,您将感到方便地将协程框架应用于编写健壮的异步代码,或通过并发运行昂贵的计算来优化您的代码。

Kotlin 协程框架不仅是一个平台特定并发和异步框架的便捷替代品。其力量在于提供统一的、通用的 API,使我们能够编写可以在 JVM、Android、JavaScript 和本地平台上运行的异步代码。

使用线程在后台执行任务

在这个食谱中,我们将探讨如何使用 Kotlin 标准库中专门为方便线程运行而设计的函数,以整洁的方式有效地与 JVM Thread 类一起工作。我们将模拟两个长时间运行的任务,并在后台线程中并发执行它们。

准备工作

在这个食谱中,我们将使用两个模拟长时间运行操作的功能。这是第一个:

private fun `5 sec long task`() = Thread.sleep(5000)

这是第二个:

private fun `2 sec long task`() = Thread.sleep(2000)

它们两者都只是分别阻塞当前线程五秒和两秒,以模拟长时间运行的任务。我们还将使用预定义的函数返回当前线程名称,用于调试目的:

private fun getCurrentThreadName(): String = Thread.currentThread().name

如何做到这一点...

  1. 让我们先记录当前线程名称到控制台:
println("Running on ${getCurrentThreadName()}")
  1. 在一个新的 Thread 中启动并调用 5 sec long task() 函数:
println("Running on ${getCurrentThreadName()}")

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
 `5 sec long task`()
 println("Ending async operation on ${getCurrentThreadName()}")
}
  1. 在另一个 Thread 中启动另一个 Thread 并调用 2 sec long task()
println("Running on ${getCurrentThreadName()}")

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
    `5 sec long task`()
    println("Ending async operation on ${getCurrentThreadName()}")
}

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
 `2 sec long task`()
 println("Ending async operation on ${getCurrentThreadName()}")
}

它是如何工作的...

以下代码将打印以下文本到控制台:

Running on main
Starting async operation on Thread-0
Starting async operation on Thread-1
Ending async operation on Thread-1
Ending async operation on Thread-0

如您所见,我们已经成功启动了两个并发运行的后台线程。我们使用来自 kotlin.concurrent 包的 thread() 工具函数,该函数负责实例化和启动一个新的线程,该线程以 lambda 表达式的形式运行传递给它的代码块。

参见

  • 查看其余的菜谱,了解如何使用 Kotlin 协程框架用更强大和灵活的框架替换线程机制。一个好的起点可以是 使用协程执行异步并发任务使用协程执行异步并发任务并处理结果 菜谱。

背景线程同步

在这个菜谱中,我们将探讨如何使用 Kotlin 标准库中专门用于以方便方式运行线程的函数,以干净的方式有效地与 JVM Thread 类一起工作。我们将模拟两个长时间运行的任务,并在后台线程中同步执行它们。

准备工作

在这个菜谱中,我们将使用以下两个函数来模拟长时间运行的操作。5 秒长任务() 函数:

private fun `5 sec long task`() = Thread.sleep(5000)

以及 2 秒长任务() 函数:

private fun `2 sec long task`() = Thread.sleep(2000)

它们各自仅负责阻塞当前线程五秒和两秒,以模拟长时间运行的任务。我们还将使用预定义的函数返回当前线程名称,用于调试目的:

private fun getCurrentThreadName(): String = Thread.currentThread().name

如何操作...

  1. 让我们先记录当前线程名称到控制台:
println("Running on ${getCurrentThreadName()}")
  1. 在其中启动一个新的 Thread 并调用 5 秒长任务() 函数:
println("Running on ${getCurrentThreadName()}")

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
 `5 sec long task`()
 println("Ending async operation on ${getCurrentThreadName()}")
}
  1. 等待线程完成:
println("Running on ${getCurrentThreadName()}")

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
    `5 sec long task`()
    println("Ending async operation on ${getCurrentThreadName()}")
}.join()
  1. 在其中启动另一个 Thread 并调用 2 秒长任务() 函数:
println("Running on ${getCurrentThreadName()}")

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
    `5 sec long task`()
    println("Ending async operation on ${getCurrentThreadName()}")
}.join()

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
 `2 sec long task`()
 println("Ending async operation on ${getCurrentThreadName()}")
}
  1. 等待线程完成:
println("Running on ${getCurrentThreadName()}")

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
    `5 sec long task`()
    println("Ending async operation on ${getCurrentThreadName()}")
}.join()

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
    `2 sec long task`()
    println("Ending async operation on ${getCurrentThreadName()}")
}.join()
  1. 在结束时测试主线程是否空闲:
println("Running on ${getCurrentThreadName()}")

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
    `5 sec long task`()
    println("Ending async operation on ${getCurrentThreadName()}")
}.join()

thread {
    println("Starting async operation on ${getCurrentThreadName()}")
    `2 sec long task`()
    println("Ending async operation on ${getCurrentThreadName()}")
}.join()

println("${getCurrentThreadName()} thread is free now")

它是如何工作的...

之前的代码将打印以下文本到控制台:

Running on main
Starting async operation on Thread-0
Ending async operation on Thread-0
Starting async operation on Thread-1
Ending async operation on Thread-1
main thread is free now

我们已经成功启动了两个同步的后台线程。为了顺序运行这两个后台线程,我们使用了 Thread.join() 函数,它只是阻塞主线程,直到后台线程完成。为了实例化和启动一个新的后台线程,我们使用了来自 kotlin.concurrent 包的 thread() 工具函数。我们向它传递一个以 lambda 表达式形式传递给它的代码块,以在线程中运行。

参见

  • 查看下一组菜谱,了解如何使用 Kotlin 协程框架用更强大和灵活的框架替换线程机制。一个好的起点可以是 使用协程执行异步并发任务使用协程执行异步并发任务并处理结果 菜谱。

使用协程进行异步、并发任务执行

在这个菜谱中,我们将探讨如何使用协程框架来安排异步、并发的任务执行。我们将学习如何同步一系列短的后台任务,以及如何同时运行昂贵、长时间运行的任务。我们将通过模拟寿司卷准备过程来发现如何一起安排阻塞和非阻塞任务。

准备工作

开始使用 Kotlin 协程的第一步是将核心框架依赖项添加到项目中:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.3' 

以下代码在 Gradle 构建脚本中声明了kotlinx-coroutines-core依赖项,该依赖项用于示例项目(github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook)。

在当前菜谱中,我们将假设我们的寿司烹饪模拟需要执行以下四个步骤:

  1. 煮米饭

  2. 准备鱼

  3. 切蔬菜

  4. 卷寿司

这些步骤将由以下函数来模拟:

private fun `cook rice`() {
    println("Starting to cook rice on ${getCurrentThreadName()}")
    Thread.sleep(10000)
    println("Rice cooked")
}

private fun `prepare fish`() {
    println("Starting to prepare fish on ${getCurrentThreadName()}")
    Thread.sleep(2000)
    println("Fish prepared")
}

private fun `cut vegetable`() {
    println("Starting to cut vegetables on ${getCurrentThreadName()}")
    Thread.sleep(2000)
    println("Vegetables ready")
}

private fun `roll the sushi`() {
    println("Starting to roll the sushi on ${getCurrentThreadName()}")
    Thread.sleep(2000)
    println("Sushi rolled")
}

我们还将使用以下函数来记录当前线程名称到控制台:

private fun `print current thread name`() {
    println("Running on ${getCurrentThreadName()}")
    println()
}

private fun getCurrentThreadName(): String = Thread.currentThread().name

为了练习的目的,我们将假设寿司卷准备过程必须满足以下要求:

  • 最长的煮饭步骤必须在后台以非阻塞方式执行

  • 鱼准备切菜步骤必须在煮饭时依次执行

  • 卷寿司步骤只能在完成前三个步骤后进行

如何操作...

  1. 让我们先记录当前线程名称到控制台:
`print current thread name`()
  1. 在后台线程池上启动一个新的协程:
`print current thread name`()
var sushiCookingJob: Job
sushiCookingJob = launch(newSingleThreadContext("SushiThread")) {
    `print current thread name`()
}
  1. 在嵌套协程中异步执行cook rice()函数:
`print current thread name`()
var sushiCookingJob: Job
sushiCookingJob = launch(newSingleThreadContext("SushiThread")) {
    `print current thread name`()
    val riceCookingJob = launch {
        `cook rice`()
 }
}
  1. 在后台运行cook rice()函数的同时,按顺序运行prepare fish()cut vegetable()函数:
`print current thread name`()
var sushiCookingJob: Job
sushiCookingJob = launch(newSingleThreadContext("SushiThread")) {
    `print current thread name`()
    val riceCookingJob = launch {
        `cook rice`()
    }
    println("Current thread is not blocked while rice is being
     cooked")
    `prepare fish`()
 `cut vegetable`()
}
  1. 等待米饭烹饪的协程完成:
`print current thread name`()
var sushiCookingJob: Job
sushiCookingJob = launch(newSingleThreadContext("SushiThread")) {
    `print current thread name`()
    val riceCookingJob = launch {
        `cook rice`()
    }
    println("Current thread is not blocked while rice is being
     cooked")
    `prepare fish`()
    `cut vegetable`()
 riceCookingJob.join()
}
  1. 调用最终的roll the sushi()函数并等待主协程完成:
`print current thread name`()
var sushiCookingJob: Job
sushiCookingJob = launch(newSingleThreadContext("SushiThread")) {
    `print current thread name`()
    val riceCookingJob = launch {
        `cook rice`()
    }
    println("Current thread is not blocked while rice is being
     cooked")
    `prepare fish`()
    `cut vegetable`()
    riceCookingJob.join()
 `roll the sushi`()
}
runBlocking {
   sushiCookingJob.join()
}
  1. 测量函数执行的总时间并将其记录到控制台:
`print current thread name`()
var sushiCookingJob: Job
val time = measureTimeMillis {
    sushiCookingJob = launch(newSingleThreadContext("SushiThread")) {
        `print current thread name`()
        val riceCookingJob = launch {
            `cook rice`()
        }
        println("Current thread is not blocked while rice is being
         cooked")
        `prepare fish`()
        `cut vegetable`()
        riceCookingJob.join()
        `roll the sushi`()
    }
    runBlocking {
        sushiCookingJob.join()
    }
}
println("Total time: $time ms")

它是如何工作的...

以下代码将在控制台打印以下文本:

Running on main
Running on SushiThread
Current thread is not blocked while rice is being cooked
Starting to cook rice on ForkJoinPool.commonPool-worker-1
Starting to prepare fish on SushiThread
Fish prepared
Starting to cut vegetables on SushiThread
Vegetables ready
Rice cooked
Starting to roll the sushi on SushiThread
Sushi rolled
Total time: 12089 ms

在开始时,我们使用launch()函数调用在后台线程上启动一个新的协程。我们还在var sushiCookingJob: Job变量下创建了launch()函数返回的Job实例的句柄。

launch()函数在默认的CoroutineContext实例上启动一个新的协程实例。然而,我们可以将我们想要的CoroutineContext作为附加参数传递给launch()函数。在针对 JVM 平台时,默认情况下,launch()函数在一个后台线程池上启动协程,这对应于CommonPool上下文常量。我们也可以通过传递newSingleThreadContext()函数的上下文结果来在单个线程上运行协程。如果你正在使用 UI 框架,如 Android、Swing 或 JavaFx,你还可以在UI上下文中运行协程。UI上下文与负责用户界面更新的主线程相关。有不同模块提供针对特定框架的UI上下文实现。你可以在以下官方指南中了解更多关于特定框架的协程 UI 编程:github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md

在主协程内部,我们启动一个新的协程,并在其中调用cook rice()函数。我们将对应于处理cook rice()函数的协程的Job实例的句柄存储在val riceCookingJob: Job变量下。此时,米饭烹饪任务开始在多个线程的线程池上并发运行。

接下来,我们调用两个函数——prepare fish()cut vegetable()。正如你在控制台输出中看到的,这些函数是顺序执行的。蔬菜切割任务在鱼准备完成后立即开始。如果我们想并发运行它们,我们需要在每个函数内部启动一个新的协程。

最后,我们通过在riceCookingJob变量上调用一个join()函数来等待米饭烹饪任务的完成。在这里,join()函数挂起主sushiCookingJob协程,直到riceCookingJob完成。在主协程解除阻塞后,紧接着调用的最后一个函数是roll the sushi()

为了等待主协程完成,我们需要在主线程上启动它之后,在sushiCookingJob实例上调用一个join()函数。然而,我们无法在协程作用域之外调用join()函数。我们需要在用runBlocking()函数启动的新阻塞协程内部调用它。

协程框架旨在允许我们以非阻塞方式执行任务。尽管我们能够在协程的作用域内编写非阻塞代码,但我们仍需要提供一个桥梁,将启动主协程的应用程序中的原始线程连接起来。我们可以使用runBlocking()函数将非阻塞的协程作用域与外部的阻塞世界连接起来。

runBlocking() 函数启动一个新的协程并阻塞当前线程,直到其完成。它旨在将常规阻塞代码与以挂起风格编写的库桥接起来。例如,它可以在 main() 函数和测试中使用。

协程可以被视为轻量级的线程替代品。在资源消耗方面,协程是轻量级的。例如,我们可以轻松地同时启动一百万个协程,其中每个协程在两秒钟后都将当前线程名称记录到控制台:

runBlocking {
    (0..1000000).map {
        launch {
            delay(1000)
            println("Running on ${Thread.currentThread().name}")
        }
    }.map { it.join() }
}

之前的代码在标准计算机上大约需要 10 秒钟才能完成。相比之下,如果我们尝试使用线程运行此代码,我们会得到 OutOfMemoryError: unable to create new native thread 异常。

参见

  • 您可以通过阅读 使用协程进行异步、并发任务执行以及结果处理 菜谱来跟进。它展示了如何异步安排返回结果的函数。

使用协程进行异步、并发任务执行以及结果处理

在这个菜谱中,我们将探索如何使用协程框架来并发运行异步操作,并学习如何正确处理它们返回的结果。我们将安排两个任务,并使用两个协程在后台运行它们。第一个任务将负责显示进度条动画。第二个任务将模拟长时间运行的计算。最后,我们将打印第二个任务返回的结果到控制台。

准备工作

开始使用 Kotlin 协程的第一步是将核心框架依赖项添加到项目中:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.3' 

之前的代码在 Gradle 构建脚本中声明了 kotlinx-coroutines-core 依赖项,该依赖项用于示例项目 (github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook)。

在当前菜谱中,我们将使用以下两个函数:

private suspend fun `calculate the answer to life the universe and everything`(): Int {
    delay(5000)
    return 42
}

private suspend fun `show progress animation`() {
    val progressBarLength = 30
    var currentPosition = 0
    while (true) {
        print("\r")
        val progressbar = (0 until progressBarLength)
                .map { if (it == currentPosition) " " else "░" }
                .joinToString("")
        print(progressbar)

        delay(50)

        if (currentPosition == progressBarLength) {
            currentPosition = 0
        }
        currentPosition++
    }
}

第一个函数是模拟一个昂贵的计算,它将线程延迟五秒钟并在最后返回结果。第二个函数负责显示无限进度条动画。我们将同时启动这两个操作并等待第一个操作返回的结果。在得到结果后,我们将将其打印到控制台。

我们还将使用以下函数将当前线程名称记录到控制台:

private fun `print current thread name`() {
    println("Running on ${getCurrentThreadName()}")
    println()
}

private fun getCurrentThreadName(): String = Thread.currentThread().name

如何做到这一点...

  1. 首先记录当前线程名称到控制台:
`print current thread name`()
  1. 启动一个协程,负责从后台显示进度条动画:
`print current thread name`()

launch {
    println("Starting progressbar animation on ${getCurrentThreadName()}")
 `show progress animation`()
}
  1. 启动一个协程,负责在后台运行 calculate the answer to life the universe and everything() 函数:
`print current thread name`()

launch {
    println("Starting progressbar animation on ${getCurrentThreadName()}")
    `show progress animation`()
}

val future = async {
    println("Starting computations on ${getCurrentThreadName()}")
 `calculate the answer to life the universe and everything`()
}

println("${getCurrentThreadName()} thread is not blocked while tasks are in progress")
  1. 等待 future 协程返回的结果并将其打印到控制台:
`print current thread name`()

launch {
    println("Starting progressbar animation on ${getCurrentThreadName()}")
    `show progress animation`()
}

val future = async {
    println("Starting computations on ${getCurrentThreadName()}")
    `calculate the answer to life the universe and everything`()
}

println("${getCurrentThreadName()} thread is not blocked while tasks are in progress")

runBlocking {
    println("\nThe answer to life the universe and everything: ${future.await()}")
 `print current thread name`()
}

它是如何工作的...

我们将编写代码以显示进度条动画五秒钟,然后一旦模拟计算完成,将打印calculate the answer to life the universe and everything()函数的结果:

Running on main
Starting progressbar animation on ForkJoinPool.commonPool-worker-1
Starting calculation of the answer to life the universe and everything on ForkJoinPool.commonPool-worker-2
main thread is not blocked while background tasks are still in progress
░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░
The answer to life the universe and everything: 42
Running on main

我们使用async()函数在后台任务中启动calculate the answer to life the universe and everything()函数的执行。它只是启动一个新的协程并返回一个Deferred<T>类的实例。泛型T类型对应于async()返回的对象类型。Deferred<T>类型的实例只是指向协程提供的未来结果的指针。它是异步编程结构的表示,称为futurepromise。我们可以通过在Deferred对象上调用await()函数来评估Deferred对象的值。然而,我们无法在协程作用域之外调用await()函数。我们需要在用runBlocking()函数启动的新阻塞协程内部调用它。

协程框架旨在允许我们以非阻塞方式执行任务。尽管我们能够在协程的作用域内编写非阻塞代码,但我们仍需要提供一个连接到启动主协程的应用程序中原始线程的桥梁。我们可以使用runBlocking()函数将非阻塞的协程作用域与阻塞的外部世界连接起来。runBlocking()函数启动一个新的协程并阻塞当前线程直到其完成。它旨在将常规阻塞代码与以挂起风格编写的库桥接起来。例如,它可以在main()函数和测试中使用。

就进度条动画而言,我们正在使用launch()函数在后台安排它。launch()负责启动一个新的协程,然而,它并不关心交付最终结果。

更多...

你可能已经注意到我们的预定义函数在fun关键字之前标记了suspend修饰符,例如,**suspend** fun `show progress animation`()。背后的原因是,我们需要明确声明函数将在协程作用域内运行,以便能够在函数体内部使用协程特定的功能。在我们的例子中,我们使用了delay()函数,它只能在协程作用域内调用。它负责暂停协程一段时间而不阻塞当前线程。

参见

  • 你可以在Applying coroutines for asynchronous data processing菜谱中调查delay()函数的另一种用法。你还可以在Easy coroutines cancelation菜谱中探索挂起函数的不同用例。

  • 如果您想了解更多关于使用协程进行并发、异步任务调度的信息,您可以查看 使用协程进行异步、并发任务执行及结果处理 菜谱。它解释了如何调度在公共协程中运行的顺序和并发任务。

应用协程进行异步数据处理

在这个菜谱中,我们将为 Iterable 类型实现一个泛型扩展,它将提供 Iterable<T>.map() 函数的替代方案。我们的 Iterable<T>.mapConcurrent() 函数实现将通过与协程并发运行来优化数据映射操作。接下来,我们将通过将其应用于模拟对样本 Iterable 对象每个元素应用耗时操作来测试我们的并发映射函数实现。

如何实现...

  1. 为泛型 Iterable<T> 类实现一个扩展函数,用于处理其元素的并发映射操作:
suspend fun <T, R> Iterable<T>.mapConcurrent(transform: suspend (T) -> R) =
    this.map {
        async { transform(it) }
    }.map {
        it.await()
    }
  1. 模拟对样本 Iterable 范围元素应用耗时映射操作:
runBlocking {
 (0..10).mapConcurrent {
        delay(1000)
        it * it
    }
}
  1. 将映射后的元素打印到控制台:
runBlocking {
        (0..10).mapConcurrent {
            delay(1000)
            it * it
        }.map { println(it) }
}
  1. 测量并发映射操作执行的总时间并将其记录到控制台:
runBlocking {
 val totalTime = measureTimeMillis {        (0..10).mapConcurrent {
            delay(1000)
            it * it
        }.map { println(it) }
    }
 println("Total time: $totalTime ms")
}

它是如何工作的...

让我们先分析一下,将我们在开头实现的 mapConcurrent() 函数应用于整数范围 (0..10) 的元素时产生的影响。在传递给 mapConcurrent 函数的 lambda 块中,我们正在模拟一个长时间运行的处理操作,通过使用 delay(1000) 函数挂起协程一秒钟,并返回原始整数值的平方。

我们的代码将打印以下结果到控制台:

0
1
4
9
16
25
36
49
64
81
100
Total time: 1040 ms

我们实现的 Iterable.mapConcurrent() 扩展函数接受一个函数参数 transform: suspend (T) -> R,它表示将要应用于 Iterable 对象原始元素的运算。在底层,为了实现并发数据转换,我们为每个原始元素使用 async() 函数启动一个新的协程,并将 transform 函数应用于它们。此时,原始的 Iterable<T> 实例已经被转换成了 Iterable<Deferred<T>> 类型。接下来,通过在它们上调用 await() 函数,将 async() 调用返回的连续 Deferred 类型实例同步,并转换为通用的 R 类型。最后,我们得到了一个返回所需 R 类型的 Iterable

如您所见,在我们的示例输出中,使用 Iterable.mapConcurrent() 函数对 10 个整数进行转换在大约一秒钟内完成,在标准计算机上。您可以尝试使用标准的 Iterable.map() 运行相同的转换,它将需要大约 10 秒钟。

为了在传递给 mapConcurrent() 函数的 transform lambda 块内部模拟延迟,我们使用带有指定时间值的 delay() 函数。delay() 函数会暂停协程一段时间,但不会阻塞线程。transform 块将为后台线程池中的每个元素执行。每当一个协程被暂停时,另一个协程就会在原地开始运行以替代第一个协程。如果我们用阻塞的 Thread.sleep(1000) 函数替换非阻塞的 delay(1000) 调用,我们的示例将大约在四秒内完成。与默认不并发运行的 Iterable.map() 函数相比,这仍然是一个巨大的进步。

协程框架旨在允许我们以非阻塞的方式执行任务。尽管我们能够在协程的作用域内编写非阻塞代码,但我们仍需要为启动主协程的应用程序中的原始线程提供一个桥梁。我们能够使用 runBlocking() 函数将非阻塞的协程作用域与外部的阻塞世界连接起来。runBlocking() 函数启动一个新的协程并阻塞当前线程,直到其完成。它旨在将常规阻塞代码与以挂起风格编写的库桥接起来。例如,它可以在 main() 函数和测试中使用。

参见

  • 如果你想要了解更多关于扩展函数机制的基础知识,你可以查看第二章中的扩展类的功能配方,表达性函数和可调整接口

简单的协程取消

在这个配方中,我们将探讨如何实现一个允许我们取消其执行的协程。我们将使用协程在后台控制台创建一个无限进度条动画。接下来,在给定延迟后,我们将取消协程并测试动画的行为。

准备工作

开始使用 Kotlin 协程的第一步是将核心框架依赖项添加到项目中:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.3' 

上述代码在 Gradle 构建脚本中声明了 kotlinx-coroutines-core 依赖项,该依赖项用于示例项目 (github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook)。

如何操作...

  1. 实现一个负责在控制台显示无限进度条动画的挂起函数:
private suspend fun `show progress animation`() {
    val progressBarLength = 30
    var currentPosition = 0
    while (true) {
        print("\r")
        val progressbar = (0 until progressBarLength)
                .map { if (it == currentPosition) " " else "░" }
                .joinToString("")
        print(progressbar)

        delay(50)

        if (currentPosition == progressBarLength) {
            currentPosition = 0
        }
        currentPosition++
    }
}
  1. 在一个新的协程中启动 show progress animation() 函数:
runBlocking {
    val job = launch { `show progress animation`() }
}
  1. 暂停父线程五秒钟:
runBlocking {
    val job = launch { `show progress animation`() }
    delay(5000)
}
  1. 取消进度条动画任务:
runBlocking {
    val job = launch { `show progress animation`() }
    delay(5000)
    job.cancel()
    println("Cancelled")
}
  1. 等待作业完成并将完成事件记录到控制台:
runBlocking {
    val job = launch {`show progress animation`()}
    delay(5000)
    job.cancel()
 job.join()
 println("\nJob cancelled and completed")
}

工作原理...

最后,我们的代码将显示一个持续五秒的进度条动画,然后停止。我们通过在由launch()函数创建的新协程实例内部调用它,来安排show progress animation()函数在后台运行。我们将launch()函数返回的Job实例的句柄存储在job变量下。

接下来,我们使用delay(5000)调用将外部的runBlocking()协程作用域挂起五秒。一旦delay()函数恢复协程执行,我们就调用负责显示进度条动画的协程Job上的cancel()函数。

在底层,在show progress animation()函数内部,我们运行一个无限while循环,每 50 毫秒更新一次最后一条控制台行,以显示新的进度条动画状态。然而,正如你可以通过运行示例来验证的那样,动画在负责运行它的Job被取消后立即停止,尽管在取消后,我们调用了join()函数来等待其完成。

你还可以使用一个名为cancelAndJoin()Job扩展函数,它将cancel()join()调用组合在一起。然而,如果你不想等待实际的协程停止事件,一个简单的cancel()调用就足够了。

参见

  • 如果你想要探索协程框架的基础知识,请查看使用协程执行异步并发任务使用协程执行异步并发任务并处理结果配方

使用 Retrofit 和协程适配器构建 REST API 客户端

在这个配方中,我们将探索如何使用协程通过 REST API 与远程端点交互。我们将使用 Retrofit 库实现 REST 客户端,允许我们异步地通过 HTTP 与 GitHub API 通信。最后,我们将将其用于实际操作,以获取给定搜索查询的 GitHub 存储库搜索结果。

准备工作

开始使用 Kotlin 协程的第一步是添加一个核心框架依赖项:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.3' 

为了使用 Retrofit 库与协程适配器插件,我们还需要将以下依赖项添加到我们的项目中:

implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-experimental-adapter:1.0.0'

之前的代码在 Gradle 构建脚本中声明了所需的依赖项,该脚本用于示例项目(github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook)。retrofit模块提供了核心 Retrofit 库实现。converter-gson添加了一个 Gson 插件,它允许自动将 JSON 响应转换为 Kotlin 模型数据类。retrofit2-kotlin-coroutines-experimental-adapter模块提供了一个用于异步 REST 调用的适配器,允许我们使用 Kotlin 协程的Deferred类型包装响应。

在这个菜谱中,我们将使用公开可用的 GitHub REST API。我们将与一个端点进行通信,该端点负责返回包含给定搜索查询的 GitHub 仓库的搜索结果。您可以在以下位置找到详细的端点文档:developer.github.com/v3/search/#search-repositories

/search/repositories端点允许我们使用GET方法访问远程资源,并通过键q传递所需的搜索短语。例如,匹配搜索短语"live.parrot"的仓库的GET请求 URL 将如下所示:https://api.github.com/search/repositories?q=parrot.live。端点返回的结果使用 JSON 格式进行格式化。您可以通过在浏览器中打开示例 URL 或使用curl命令行工具来查看原始响应的外观:curl https://api.github.com/search/repositories?q=parrot.live

如何做到这一点...

  1. 声明数据类来模拟服务器响应:
data class Response(@SerializedName("items")
                                      val list: Collection<Repository>)
data class Repository(val id: Long?,
                      val name: String?,
                      val description: String?,
                      @SerializedName("full_name") val fullName:
                       String?,
                      @SerializedName("html_url") val url: String?,
                      @SerializedName("stargazers_count") val stars:
                       Long?)
  1. 声明一个模拟 GitHub 端点使用的接口:
interface GithubApi {
    @GET("/search/repositories")
    fun searchRepositories(@Query("q") searchQuery: String):
    Deferred<Response>

}
  1. 使用Retrofit类实例化GithubApi接口:
val api: GithubApi = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .build()
        .create(GithubApi::class.java)
  1. 使用GithubApi实例调用端点,并传递搜索短语"kotlin"
val api: GithubApi = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .build()
        .create(GithubApi::class.java)

api.searchRepositories("Kotlin")
  1. 等待响应并获取到Repository类对象的列表引用:
val api: GithubApi = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .build()
        .create(GithubApi::class.java)

val downloadedRepos = api.searchRepositories("Kotlin").await().list
  1. 按照仓库的星级数量降序排序仓库列表,并将它们打印到控制台:
val api: GithubApi = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .build()
        .create(GithubApi::class.java)

val downloadedRepos = api.searchRepositories("Kotlin").await().list
downloadedRepos
 .sortedByDescending { it.stars }
 .forEach {
 it.apply {
 println("$fullName ![](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/kt-stdlib-cb/img/a541e4e1-5cd5-430c-8778-8f64c957e4a2.png)$stars\n$description\n$url\n")
 }
 }

它是如何工作的...

因此,我们的代码将向服务器发送请求,获取并处理响应,并将以下结果打印到控制台:

JetBrains/kotlin ![](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/kt-stdlib-cb/img/f20aeb9f-ae96-4ab4-81ae-59a08f467e67.png)23051
The Kotlin Programming Language
https://github.com/JetBrains/kotlin

perwendel/spark ![](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/kt-stdlib-cb/img/f20aeb9f-ae96-4ab4-81ae-59a08f467e67.png)7531
A simple expressive web framework for java. News: Spark now has a kotlin DSL https://github.com/perwendel/spark-kotlin
https://github.com/perwendel/spark

KotlinBy/awesome-kotlin ![](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/kt-stdlib-cb/img/f20aeb9f-ae96-4ab4-81ae-59a08f467e67.png)5098
A curated list of awesome Kotlin related stuff Inspired by awesome-java. 
https://github.com/KotlinBy/awesome-kotlin

ReactiveX/RxKotlin ![](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/kt-stdlib-cb/img/f20aeb9f-ae96-4ab4-81ae-59a08f467e67.png)4413
RxJava bindings for Kotlin
https://github.com/ReactiveX/RxKotlin

JetBrains/kotlin-native ![](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/kt-stdlib-cb/img/f20aeb9f-ae96-4ab4-81ae-59a08f467e67.png)4334
Kotlin/Native infrastructure
https://github.com/JetBrains/kotlin-native
 ...

我们首先实现了表示服务器 JSON 响应中返回的数据的模型类。你可能已经注意到,一些属性被标记了@SerializedName()注解。这个注解的目的是告诉 Gson 库,指定的属性应该从与@SerializedName()传递的值匹配的 JSON 字段反序列化。接下来,我们声明了一个接口,GithubApi,它表示我们想要用来与端点通信的方法。我们声明了一个名为searchRepositories的单个方法,它接受一个String参数,该参数对应于存储库搜索端点所需的搜索查询值。我们还用@GET注解标记了searchRepositories方法,它指定了要使用的 REST 方法类型和端点的路径。searchRepositories方法应该返回一个Deferred<Response>类型的实例,表示异步调用的未来结果。GithubApi接口的实现是由 Retrofit 库内部生成的。为了获取GithubApi实例,我们需要实例化Retrofit类型,并使用端点的 URL 地址和负责 JSON 反序列化和对服务器执行异步调用的机制进行配置。最后,我们调用Retrofit.create(GithubApi::class.java)来获取GithubApi实例。就是这样!

为了执行对服务器的实际调用,我们需要调用GithubApi.searchRepositories()函数:

api.searchRepositories("Kotlin")

接下来,为了从响应中获取Repository对象列表,我们需要等待对服务器的异步调用完成以及响应解析:

val downloadedRepos = api.searchRepositories("Kotlin").await().list

最后,我们对从响应中获取的存储库列表进行后处理。我们按星级数量降序排序,并使用以下代码将其打印到控制台:

val downloadedRepos = api.searchRepositories("Kotlin").await().list
downloadedRepos
 .sortedByDescending { it.stars }
        .forEach {
            it.apply {
                println("$fullName ![](https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/kt-stdlib-cb/img/f20aeb9f-ae96-4ab4-81ae-59a08f467e67.png)$stars\n$description\n$url\n")
 }
        } 

参见

  • 如果你想要探索协程框架的基础知识,可以查看使用协程执行异步并发任务执行使用协程执行异步并发任务执行并处理结果配方。你可以通过探索其主页square.github.io/retrofit/来了解更多关于 Retrofit 库的信息,其中包含有用的示例。

使用协程包装第三方回调式 API

通常,第三方库提供回调式异步 API。然而,回调函数被认为是一种反模式,尤其是在我们处理多个嵌套回调时。在这个配方中,我们将学习如何处理提供回调式方法的库,通过轻松地将它们转换为可以使用协程运行的挂起函数。

准备中

开始使用 Kotlin 协程的第一步是将核心框架依赖项添加到项目中:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.3' 

上述代码在 Gradle 构建脚本中声明了kotlinx-coroutines-core依赖,该依赖用于示例项目(github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook)。

就食谱任务而言,让我们假设我们有一个名为Result的类,其定义如下:

data class Result(val displayName: String)

这里是getResultsAsync()函数,它模拟第三方回调风格的 API:

fun getResultsAsync(callback: (List<Result>) -> Unit) =
    thread {
        val results = mutableListOf<Result>()

        // Simulate some extensive bacground task
        Thread.sleep(1000)

        results.add(Result("a"))
        results.add(Result("b"))
        results.add(Result("c"))

        callback(results)
    }

getResultsAsync()函数只是启动一个后台线程,延迟一秒钟,然后调用传递给它作为参数的回调函数,将Result类对象的列表传递给它。

如何操作...

  1. 使用挂起函数包装getResultsAsync()函数,直接返回结果:
suspend fun getResults(): List<Result> =
    suspendCoroutine { continuation: Continuation<List<Result>> ->
        getResultsAsync { continuation.resume(it) }
    }

  1. 在协程中启动一个协程并调用其中的getResults()挂起函数:
val asyncResults = async {
    getResults()
}
  1. 等待结果并打印到控制台:
val asyncResults = async {
    getResults()
}

println("getResults() is running in bacground. Main thread is not blocked.")
asyncResults.await().map { println(it.displayName) }
println("getResults() completed")

它是如何工作的...

最后,我们的代码将打印以下输出到控制台:

getResults() is running in bacground. Main thread is not blocked.
a
b
c
getResults() completed
Total time elapsed: 1029 ms

我们成功地将回调风格的getResultsAsync(callback: (List<Result>) -> Unit)函数转换成了直接返回结果的干净形式的挂起函数–suspend fun getResults(): List<Result>。为了去掉原始的callback参数,我们使用了标准库提供的suspendCoroutine()函数。suspendCoroutine()函数接受block: (Continuation<T>) -> Unit函数类型作为参数。Continuation接口被设计用来允许我们恢复由挂起函数暂停的协程。

当在协程内部调用suspendCoroutine函数时,它会捕获其执行状态到一个Continuation实例中,并将这个延续传递给指定的块作为参数。为了恢复协程的执行,块可以调用continuation.resume()continuation.resumeWithException()

我们在传递给suspendCoroutine()函数的 lambda 中调用原始的getResultsAcync()函数,并在传递给getResultsAsync()函数作为参数的callback lambda 阻塞中调用continuation.resume(it)函数:

suspend fun getResults(): List<Result> =
    suspendCoroutine { continuation: Continuation<List<Result>> ->
        getResultsAsync { continuation.resume(it) }
    }

作为结果,调用getResults()的协程将挂起,直到getResultsAsync()函数内部执行callback lambda。

参见

  • 如果你想探索协程框架的基础,请查看使用协程执行异步、并发任务使用协程执行异步、并发任务并处理结果的食谱

第八章:Android、JUnit 和 JVM UI 框架的最佳实践

在本章中,我们将涵盖以下菜谱:

  • 使用 Android Extensions 插件进行干净且安全的 View 绑定

  • 在 Android 上、JavaFX 和 Swing 中应用协程进行异步 UI 编程

  • 使用 @Parcelize 注解在 Android 上轻松进行类序列化

  • 实现提供生命周期感知值的自定义属性代理

  • SharedPreferences 上进行轻松操作

  • 更少的样板 Cursor 数据解析

  • 使用 Mockito Kotlin 库模拟依赖项

  • 验证函数调用

  • Kotlin 协程的单元测试

简介

当前章节将解决 Kotlin 最常使用的流行框架的特定问题。总的来说,它将侧重于 Android 平台特定的方面,以及在 Android 和 JVM 框架(如 JavaFX 和 Swing)上的异步 UI 编程,同时也会指导您使用 JUnit 框架(junit.org/junit5/) 为 JVM 平台编写有效的单元测试。与单元测试相关的菜谱还将包括更高级的主题,例如使用 mockito-kotlin (github.com/nhaarman/mockito-kotlin) 库模拟依赖项、基于协程框架测试异步代码,以及使用标准库提供的断言。

使用 Android Extensions 插件进行干净且安全的视图绑定

在这个菜谱中,我们将探索 Kotlin Android Extensions 插件提供的视图绑定功能。它允许我们以简单且健壮的方式获取在 XML 布局文件中声明的 View 类型元素的引用,而不使用原始的 findViewById() 函数。我们将声明一个 TextView 元素在 Activity 布局中,并获取其引用以在其中显示示例文本。

准备工作

为了使用 Kotlin Android Extensions 插件,我们需要在 Android 项目模块级别的 build.gradle 脚本中启用它,通过添加以下声明:

apply plugin: 'kotlin-android-extensions'

您可以在本书 GitHub 仓库中找到的 AndroidSamples 项目中检查与 Android 框架相关的菜谱的实现和配置:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook/。要遵循与 Android 相关的菜谱,您只需在 Android Studio 中创建一个新的项目。

如何做到...

  1. 在项目中创建一个新的 Activity:
class MainActivity : AppCompatActivity() {}
  1. src/main/res/layout/ 目录下的 activity_main.xml 文件中实现 UI 布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout     xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

 <TextView
        android:id="@+id/text_field"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="56sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. onCreate() 钩子函数中设置 MainActivity 的布局:
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
  1. 获取在 XML 布局中声明的 TextView 的引用并在其中显示示例文本:
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        text_field.text = "Bonjour!"
    }
}

它是如何工作的...

因此,MainActivity 类将在 TextView 中显示问候语:

在底层,Android 扩展插件为 activity_main.xml 文件中声明的每个 View 元素为 MainActivity 类生成了扩展属性。生成的属性与它们对应布局元素的 ID 名称相同。

与使用 findViewById(): View 函数获取 View 类引用的标准方式相比,Android 扩展绑定机制更加简洁和易于忍受。它还非常安全和健壮,因为它不需要将 View 类型强制转换为特定的子类,并且每当 XML 布局文件有任何更改时,它都会重新生成所有扩展属性。此外,与其他第三方视图绑定库相比,它更容易使用,因为它不需要任何手动属性声明。它只需无缝工作。

还有更多...

默认情况下,Android 扩展插件支持 ActivityFragmentView 类型容器,在这些容器中你可以直接使用自动视图绑定机制。然而,通过实现 LayoutContainer 接口,你可以使用任何类作为 Android 扩展容器。例如,它可以用在 RecyclerView.ViewHolder 的子类中:

class ViewHolder(override val containerView: View) : ViewHolder(containerView),
    LayoutContainer {  
    fun setupItemView(title: String) {  itemTitle.text = "Hello World!"  } 
}

你可以在官方参考中了解更多关于 Android 扩展应用程序的信息:kotlinlang.org/docs/tutorials/android-plugin.html

在 Android、JavaFX 和 Swing 上应用协程进行异步 UI 编程

大多数基于 JVM 的 GUI 框架有一个共同点——它们运行一个特定的线程,该线程负责更新应用程序 UI 的状态。在这个菜谱中,我们将学习如何在后台异步执行任务,并切换到 UI 线程以更新应用程序的 GUI。我们将创建一个简单的计数器,它将每秒显示递增的整数值。负责无限计数器递增的机制应在后台运行,但是每当它需要更新 UI 状态时,它应该切换到 UI 线程上下文。

准备工作

开始使用 Kotlin 协程的第一步是将核心框架依赖项添加到项目中:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.3' 

除了 Coroutines 核心依赖项之外,我们还需要添加一个框架特定的协程子模块,提供负责在 UI 线程上调度协程的协程上下文实现。你可以在官方指南中找到协程框架子模块的列表:github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md。在这个菜谱中,我们将针对 Android 平台,然而,你可以轻松地将示例代码移植到支持的框架之一,例如 Android、Swing 或 JavaFx。

你可以在 GitHub 仓库中查看与 Android 框架相关的配方实现和配置,该仓库位于 AndroidSamples 项目:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook/。要遵循与 Android 相关的配方,你只需要在 Android Studio 中创建一个新的项目。

如何做到这一点...

  1. 添加一个新的 Activity 子类:
class MainActivity: AppCompatActivity() {}
  1. src/main/res/layout/目录下的activity_main.xml文件中实现 UI 布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

 <TextView
        android:id="@+id/text_field"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="56sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

 <Button
            android:id="@+id/cancel_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Cancel"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
  1. onCreate()钩子函数内设置MainActivity的布局:
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
  1. 在后台启动一个新的协程,每秒增加计数器并在从 XML 布局中获得的TextView中显示:
class MainActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val job = launch {
 var counter = 1
            while (true) {
 delay(1000)
 counter++
 withContext(UI) {
                    text_field.text = counter.toString()
 }
            }
        }
    }
}
  1. 通过点击取消按钮允许协程取消:
class MainActivity: AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        text_field.text = "Bonjour!"

        val job = launch {
            var counter = 1
            while (true) {
                delay(1000)
                counter++
                withContext(UI) {
                    text_field.text = counter.toString()
                }
            }
        }

 cancel_btn.setOnClickListener {
            job.cancel()
 }
    }
}

它是如何工作的...

MainActivity.onCreate()函数中启动的协程正在运行一个无限while循环。每次迭代都以一分钟延迟和计数器变量的增加开始。接下来,我们使用withContext()函数来更新TextView的新值。

withContext()函数允许我们切换到从上下文参数获得的新协程调度器,以执行传递给它的代码块。它不会创建和启动一个新的协程,而是立即修改父协程的上下文。新调度器仅临时应用于执行给定的代码块。在withContext()函数调用之后,在协程作用域内执行的任何进一步操作都将使用原始协程上下文运行。

我们将协程返回的Job实例分配给job变量。接下来,我们设置取消按钮的监听器。每次点击取消按钮时,都会在协程Job引用上调用cancel()函数。

因此,我们的MainActivity实现将每秒更新TextView的值。一旦点击取消按钮,更新机制将立即停止。

还有更多...

如果你正在使用不同的 JVM 框架开发应用程序,为了从后台切换到 UI 线程,你可以使用带有JavaFxSwing常量的withContext()函数,而不是 Android 的UI上下文常量。

参见

  • 如果你想探索协程框架的基础知识,你应该查看第七章中的配方,让异步编程再次伟大

使用@Parcelize注解在 Android 上轻松实现类序列化

在这个配方中,我们将使用@Parcelize注解来简化 Android Parcelable接口的实现,使我们能够有效地序列化对象。@Parcelize在 Kotlin Android Extensions 插件中可用,并为实现Parcelable接口的 Kotlin 类提供自动代码生成。

准备工作

我们将实现 Android 仪器化测试用例,以验证类序列化和反序列化在实际中的效果。为了使用 Android KTX 库,我们需要将其添加到项目依赖项中。在我们的例子中,我们将在 android-test 模块中使用它。我们可以通过以下声明添加它:

androidTestImplementation 'androidx.core:core-ktx:1.0.+'

为了使用 Kotlin Android 扩展插件,我们需要在 Android 项目模块级别的 build.gradle 脚本中启用它,通过添加以下声明:

apply plugin: 'kotlin-android-extensions'

您可以在书中 GitHub 仓库中找到的 AndroidSamples 项目中检查与 Android 框架相关的食谱的实现和配置:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook/。要遵循与 Android 相关的食谱,您只需在 Android Studio 中创建一个新的项目。

如何实现...

  1. 让我们从创建一个使用 @Parcelize 注解实现 Parcelable 接口的示例 User 类开始:
@Parcelize
data class User(val name: String, val address: Address): Parcelable

@Parcelize
data class Address(val street: String, 
                   val number: String, 
                   val city: String): Parcelable
  1. 通过编写和读取 Bundle 实例来验证 User 类实例的序列化和反序列化:
@Test
fun testUserParcelisation() {
    // given
    val originalUser = User("Bob", Address("Rue de Paris", "123",
     "Warsaw"))
    val bundle = Bundle()

    // when
    bundle.putParcelable("my_user", originalUser)

    // then
    val deserialisedUser = bundle.get("my_user") as User
    assertEquals(originalUser, deserialisedUser)
} 

它是如何工作的...

我们首先定义了 User 类,其中包含 Address 类的属性。UserAddress 都被 @Parcelize 注解装饰。它告诉 Android 扩展插件生成 Parcelable 接口实现的代码。在 testUserParcelisation() 函数内部,我们创建了一个 User 类的实例,并使用 Android 的 Bundle 机制对其进行序列化。我们将原始的 User 类实例放在 "my_user" 键下,然后通过调用 bundle.get("my_user") as User 反序列化其实例。最后,我们使用 assertEquals() 函数比较原始和反序列化的 User 实例。

@Parcelize 处理为以下类型生成 Parcelable 实现:

所有原始类型、StringCharSequence、对象和枚举、ExceptionSizeSizeFBundleIBinderIInterfaceFileDescriptorSparseArraySparseIntArraySparseLongArraySparseBooleanArray。它还支持任何 Serializable 类型(例如,java.util.Date),以及 CollectionArray 类型。它还适用于可空类型。

参见

实现一个自定义属性委托,提供生命周期感知的值

通常,我们需要声明一个类属性,该属性应依赖于 ActivityFragment 的生命周期状态。在这个菜谱中,我们将使用 Kotlin 的 Lazy 代理和 Android 架构组件库提供的 Lifecycle 类 (developer.android.com/topic/libraries/architecture/)。我们将实现一个自定义属性代理,它将以懒加载的方式提供值。这意味着它们只会在第一次调用时实例化。此外,一旦 ActivityFragment 被销毁,我们将清除它们的值。这将避免由使用标准 Lazy 代理管理依赖于 Context 实例的属性而导致的内存泄漏。

准备工作

使用标准库提供的 lazy() 函数初始化的基本 Lazy 代理提供了声明非空类型属性的能力,该属性只能在某个生命周期事件之后实例化。例如,我们仅在 Activity.onCreate() 钩子函数内部设置布局之后,在属性中引用屏幕布局的元素。

然而,如果属性内部持有 Activity 实例的引用,则使用 Lazy 实现的这种实现将导致内存泄漏,因为它不允许垃圾收集器删除它。这是因为懒代理正在缓存它所持有的实例。我们将实现自己的属性代理,称为 LifeCycleAwareLazy,它将扩展 Lazy 接口,并在活动即将被销毁时清除它所持有的值。

我们将使用由 Google 提供的 Android 架构组件中的 Lifecycle 库模块。我们需要将其添加到模块级别的 build.gradle 脚本中的项目依赖项:

implementation "android.arch.lifecycle:runtime:1.1.1"

如何实现...

  1. 声明 LifecycleAwareLazy 类:
class LifecycleAwareLazy<T>(lifecycle: Lifecycle, val initializer: () -> T):             Lazy<T>, GenericLifecycleObserver
  1. init 块内注册观察者到给定的 Lifecycle 实例:
class LifecycleAwareLazy<T>(lifecycle: Lifecycle, val initializer: () -> T):             Lazy<T>, GenericLifecycleObserver {
    init {
 lifecycle.addObserver(this)
 }
}
  1. 实现一个表示代理当前存储值的内部字段:
class LifecycleAwareLazy<T>(lifecycle: Lifecycle, val initializer: () -> T):             Lazy<T>, GenericLifecycleObserver {

    init {
        lifecycle.addObserver(this)
    }

    private object UNINITIALIZED_VALUE
 private var _value: Any? = UNINITIALIZED_VALUE }
  1. 实现 Lazy 接口所需的 value 属性和 isInitialized() 函数:
class LifecycleAwareLazy<T>(lifecycle: Lifecycle, val initializer: () -> T): Lazy<T>, GenericLifecycleObserver {

    init {
        lifecycle.addObserver(this)
    }

    private object UNINITIALIZED_VALUE
    private var _value: Any? = UNINITIALIZED_VALUE

 @get:Synchronized
    override val value: T get() {
 if (_value === UNINITIALIZED_VALUE) {
 _value = initializer.invoke()
 }
 return _value as T
        }

 override fun isInitialized(): Boolean = _value != UNINITIALIZED_VALUE
}

  1. 实现 GenericLifecycleObserver 接口:
class LifecycleAwareLazy<T>(lifecycle: Lifecycle, val initializer: () -> T): Lazy<T>, GenericLifecycleObserver {

    init {
        lifecycle.addObserver(this)
    }

    private object UNINITIALIZED_VALUE
    private var _value: Any? = UNINITIALIZED_VALUE

    @get:Synchronized
    override val value: T
        get() {
            if (_value === UNINITIALIZED_VALUE) {
                _value = initializer.invoke()
            }
            return _value as T
        }

    override fun isInitialized(): Boolean = _value != UNINITIALIZED_VALUE

    override fun onStateChanged(source: LifecycleOwner?, event: Lifecycle.Event?) {
 when (event) {
 Lifecycle.Event.ON_STOP -> {
 _value = UNINITIALIZED_VALUE
 }
 else -> return
        }
 }
}

它是如何工作的...

我们实现的LifecycleAwareLazy类可以看作是标准Lazy代理实现的扩展版本。它观察由构造函数传递给它的Lifecycle实例发出的事件,并相应地处理值。内部,它包含一个私有可变属性_value: Any?,初始设置为UNINITIALIZED_VALUE对象,表示空状态。_value属性反映了委托属性的当前状态,可以是已初始化或未初始化。LifecycleAwareLazy类还公开了一个不可变的value属性,该属性负责返回委托属性的最终值。注意,它被标记为@get:Synchronized注解,这会通知编译器为这个属性生成线程安全的获取器函数。

value属性获取器内部,检查_value属性当前的值。每当它等于UNINITIALIZED_VALUE时,首先将其重新分配为构造函数中传递的initialiser函数的结果,然后作为委托属性的值返回。

Lifecycle是一个包含关联组件(如活动或片段)当前生命周期状态的类。它允许其他对象通过订阅状态更改事件来观察此状态,通过将回调传递给Lifecycle.addObserver()函数。你也可以通过访问Lifecycle.currentState属性来获取当前状态。

init块中,我们正在订阅通过LifecycleAwareLazy构造函数参数传递的Lifecycle对象的州更新。我们通过使用GenericLifecycleObserver实现将LifecycleAwareLazy实例传递给lifecycle.addObserver(this)函数。

我们通过在LifecycleAwareLazy类内部覆盖onStateChanged()函数来实现GenericLifeObserver接口。正如你所看到的,每当Lifecycle.Event.ON_STOP事件被触发时,即活动即将被销毁,我们都会将可变属性_value更新为UNINITIALIZED_VALUE对象。这样,我们可以确保即使_value属性直接或间接地持有活动Context实例的引用,它也不会阻止活动或片段被垃圾回收。与标准的懒加载代理相比,这是一个巨大的优势,因为标准的懒加载代理可能导致潜在的内存泄漏。

参见

  • 如果你想要熟悉属性委托模式的基础知识,请查看第五章中的实现委托类属性配方,采用 Kotlin 概念的优雅设计模式

在 SharedPreferences 上的简单操作

在这个菜谱中,我们将使用由 Google 开发的 Android KTX 库,它提供了一套针对 Android 应用开发的实用扩展和工具。我们将应用扩展函数,允许我们以干净和健壮的方式操作 SharedPreferences 类。

准备工作

为了使用 Android KTX 库,我们需要将其添加到项目的依赖项中。在我们的例子中,我们将在 android-test 模块中使用它。我们可以通过以下声明来添加它:

androidTestImplementation 'androidx.core:core-ktx:1.0.+'

我们将实现 Android 仪器测试用例,以验证我们对 SharedPreferences 执行的操作的效果。你可以在 GitHub 仓库中找到的 AndroidSamples 项目中检查与 Android 框架相关的菜谱的实现和配置:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook/。要遵循与 Android 相关的菜谱,你只需要在 Android Studio 中创建一个新的项目。

如何操作...

  1. 创建一个返回 SharedPreferences 实例的函数:
fun getDefaultSharedPreferences() =                                 PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getContext())
  1. 将一个示例字符串保存到 SharedPreferences 实例中:
@Test
fun testUserParcelization() {
 val prefs = getDefaultSharedPreferences()
 val userName: String = "Gonzo"
    prefs.edit {
        putString("user_name", userName)
 }
}
  1. 验证字符串是否已成功保存:
@Test
fun testSharedPrefs() {
    val prefs = getDefaultSharedPreferences()
    val userName: String = "Gonzo"
    prefs.edit {
        putString("user_name", userName)
    }

    val DEFAULT_VALUE = "empty"
    val fetchedUserName = prefs.getString("user_name",
     DEFAULT_VALUE)
 assertSame(userName, fetchedUserName)
}

它是如何工作的...

我们正在使用 KTX 库为 SharedPreferences 类提供的 edit() 扩展函数。它接受一个 lambda 块,包括我们想要在 SharedPreferences.Editor 实例上执行的操作,并自动调用 SharedPreferences.Editor.apply() 函数来提交事务。传递给 edit() 函数的 lambda 块实现了类型 SharedPreferences.Editor.() -> Unit,这允许我们通过隐式的 this 修饰符访问 Editor 实例。

如果你希望使用阻塞的 commit() 方法而不是异步的 apply() 函数来提交对 Editor 的操作,你应该将额外的 commit = true 参数传递给 edit() 函数。

参见

更少的样板 Cursor 数据解析

在这个菜谱中,我们将学习如何以更有效和简单的方式在 Android 中使用 Cursor 类型。我们将为 Cursor 类型创建一个扩展函数,允许我们以干净的方式查询它。我们还将实现一个实际示例,展示如何访问系统内容解析器以获取设备上存储的联系人,并将 Cursor 转换为表示联系人名称的字符串列表。

准备工作

你可以在 GitHub 仓库中找到的 AndroidSamples 项目中检查与 Android 框架相关的菜谱的实现和配置:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook/。要遵循与 Android 相关的菜谱,你只需要在 Android Studio 中创建一个新的项目。

如何做到这一点...

  1. 实现一个扩展函数,允许我们从 Cursor 中获取请求的列名值:
fun Cursor.getString(columnName: String): String? {
    return getString(getColumnIndex(columnName))
}
  1. 获取指向系统联系人表的 Cursor 实例:
val NOT_SPECIFIED = ""
val content = getContext().contentResolver
val projection = arrayOf(ContactsContract.Data.DISPLAY_NAME)
val cursor =
        content.query(ContactsContract.Contacts.CONTENT_URI,
                projection,
                NOT_SPECIFIED,
                emptyArray(),
                NOT_SPECIFIED)
  1. cursor 实例上调用 use 函数,并在其作用域内迭代数据:
val NOT_SPECIFIED = ""
val content = getContext().contentResolver
val projection = arrayOf(ContactsContract.Data.DISPLAY_NAME)
val cursor =
        content.query(ContactsContract.Contacts.CONTENT_URI,
                projection,
                NOT_SPECIFIED,
                emptyArray(),
                NOT_SPECIFIED)

val contacts = cursor.use {
    val contactsList = mutableListOf<String?>()
 while (it.moveToNext()) {
 val contactName = it.getString(ContactsContract.Data.DISPLAY_NAME)
 contactsList.add(contactName)
 }
 contactsList
}

它是如何工作的...

我们正在使用标准库提供的 use() 扩展函数来在 Cursor 实例上执行一系列操作。use() 可以在任何实现了 Closeable 接口类上调用。内部,在执行传递给它的 lambda 块之后,use() 会自动调用它所调用的对象的 close() 函数。多亏了这一点,我们可以在 Cursor 实例上安全地执行任何操作,并且可以确信,即使其中一些操作失败或抛出异常,光标最终也会被关闭。

use() 函数的作用域内,我们通过 while 循环迭代光标,每次迭代都将光标移动到下一行。对于每一行,我们使用 getString() 扩展函数从光标中获取当前的联系人显示名称。这允许我们通过将 Cursor.getString()Cursor.getColumnIndex() 结合起来避免代码重复。

使用 Mockito Kotlin 库模拟依赖项

经常在编写复杂类的单元测试用例时,我们会遇到一个问题,即需要实例化大量我们想要测试的类所依赖的属性。虽然这个问题可以通过依赖注入来解决,但模拟特定对象的行为而不实例化它更快、更高效、更受欢迎。在这个菜谱中,我们将探讨如何使用 Mockito Kotlin 库在编写一个包含内部依赖项的简单注册表单的单元测试时模拟依赖项。

准备工作

我们将使用 JUnit 库,它为运行测试用例类提供核心框架。我们需要通过在 gradle.build 脚本中声明它来将其添加到我们项目的依赖项列表中:

implementation group: 'junit', name: 'junit', version: '4.12'

为了使用 Kotlin Mockito 库,我们可以通过以下声明将其添加到项目的依赖项中:

implementation 'com.nhaarman:mockito-kotlin:1.5.0'

您可以在 GitHub 仓库中找到的 AndroidSamples 项目中查看与 Android 框架相关的食谱的实现和配置:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook/。要遵循与 Android 相关的食谱,您只需在 Android Studio 中创建一个新的项目。

在这个食谱中,我们将为以下声明的RegistrationFormController类编写单元测试:

class RegistrationFormController(val api: RegistrationApi) {
    var currentEmailAddress: String = ""

    fun isEmailValid(): Boolean = currentEmailAddress.contains("@")

    fun checkIfEmailCanBeRegistered(): Boolean =
        isEmailIsValid() && api.isEmailAddressAvailable(currentEmailAddress)
}

RegistrationApi接口定义如下:

interface RegistrationApi {
    fun isEmailAddressAvailable(email: String): Boolean
} 

由于我们不希望为了实例化RegistrationFormController类而实现RegistrationApi接口,我们将使用 Mockito Kotlin 的mock()函数来模拟它。

如何做...

  1. 创建一个新的测试类:
class MyTest {
}
  1. 创建一个RegistrationApi接口的模拟实例作为测试类的属性:
class MyTest {
 private val api = mock<RegistrationApi>()
}
  1. 添加一个RegistrationFormController类型的类属性:
class MyTest {
    private val api = mock<RegistrationApi>()
    private var registrationFormController = 
        RegistrationFormController(api = api)
}
  1. 创建一个测试方法来检查checkIfEmailCanBeRegistered()方法在无效电子邮件地址出现时是否表现正确:
class MyTest {
    private val api = mock<RegistrationApi>()
    private lateinit var registrationFormController: RegistrationFormController

    @Before
    fun setup() {
        registrationFormController = RegistrationFormController(api = api)
    }

    @Test
    fun `email shouldn't be registered if it's not valid`() {
 // given
        assertNotNull(registrationFormController)
 whenever(api.isEmailAddressAvailable(anyString())) doReturn(true)
 // when
        registrationFormController.currentEmailAddress = "Hilary"
        // then
        assertFalse(registrationFormController.checkIfEmailCanBeRegistered())
 } }

它是如何工作的...

email shouldn't be registered if it's not valid测试方法中,我们设置我们的模拟RegistrationApi属性,使其在isEmailAddressAvailable()函数被调用时返回true,无论传递给它的字符串值是什么。接下来,我们将RegistrationFormController类的currentEmailAddress属性更新为无效的电子邮件地址值。测试将通过,因为isEmailIsValid()函数工作正常,并且对于给定的电子邮件地址值返回false

如您所见,得益于模拟,我们避免了实现被测试类的依赖项。这是一种适当的技巧,它允许我们在模拟依赖项期望行为的同时测试业务逻辑的特定部分。当依赖项是特定于不兼容纯 JVM(即 Android)平台的平台时,模拟也可能很有用。

相关内容

  • 您可以查看验证函数调用食谱,以了解如何检查是否观察到与模拟依赖项的任何特定交互

验证函数调用

除了在测试方法中模拟依赖项的特定行为之外,模拟还允许我们验证模拟对象的具体函数是否被调用。在这个食谱中,我们将为简单的注册表单控制器编写单元测试。注册表单包含两个内部依赖项,我们将使用 Mockito Kotlin 库来模拟它们。我们将测试在不同场景下是否调用了适当的函数。

准备工作

我们将使用 JUnit 库来提供一个核心框架,用于运行测试用例类。我们需要通过在gradle.build脚本中声明它来将其添加到我们的项目依赖项列表中:

implementation group: 'junit', name: 'junit', version: '4.12'

为了使用 Kotlin Mockito 库,我们可以通过以下声明将其添加到项目依赖项中:

implementation 'com.nhaarman:mockito-kotlin:1.5.0'

您可以在 GitHub 仓库中找到的 AndroidSamples 项目中查看与 Android 框架相关的菜谱的实现和配置:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook/。要遵循与 Android 相关的菜谱,您只需在 Android Studio 中创建一个新的项目。

在这个菜谱中,我们将编写一个针对RegistrationFormController类的单元测试,该类声明如下:

class RegistrationForm(val api: RegistrationApi, val view: TextView) {
    var currentEmailAddress: String by 
        Delegates.observable("", ::onEmailAddressNewValue)

    fun onEmailAddressNewValue(prop: KProperty<*>, old: String,
     new: String) {
        if (checkIfEmailCanBeRegistered()) {
            view.showSuccessMessage("Email address is available!")
        } else {
            view.showErrorMessage("This email address is not
             available.")
        }
    }

    fun checkIfEmailCanBeRegistered(): Boolean =
            isEmailIsValid() && api.isEmailAddressAvailable(currentEmailAddress)

    fun isEmailIsValid(): Boolean = currentEmailAddress.contains("@")

}

它包含一个RegistrationApi属性,该属性定义为以下接口:

interface RegistrationApi {
    fun isEmailAddressAvailable(email: String): Boolean
} 

以及以下方式定义的TextView类型属性:

interface TextView {
    fun showSuccessMessage(message: String)
    fun showErrorMessage(message: String)
}

由于我们不想为了在测试中实例化RegistrationFormController类而实现RegistrationApiTextView接口,我们将使用 Mockito Kotlin 的mock()函数来模拟它们。

如何做到这一点...

  1. 创建一个新的测试类:
class MyTest {
}
  1. 创建一个RegistrationApi接口的模拟实例作为测试类的属性:
class MyTest {
    private val api = mock<RegistrationApi>()
}
  1. 创建一个模拟的TextView实例:
class MyTest {
    private val api = mock<RegistrationApi>()
 private val view = mock<TextView>()
}
  1. RegistrationFormController对象作为MyTest类的属性创建:
class MyTest {
    private val api = mock<RegistrationApi>()
    private val view = mock<TextView>()
    private var registrationForm = RegistrationForm(api, view)
}
  1. 添加一个测试方法以验证如果地址可用,是否会显示成功消息:
class MyTest {
    private val api = mock<RegistrationApi>()
    private val view = mock<TextView>()
    private var registrationForm = RegistrationForm(api, view)

 @Test
    fun `should display success message when email address is available`() {
 // given
        assertNotNull(registrationForm)
 // when we update the currentEmailAddress to any String
        whenever(api.isEmailAddressAvailable(ArgumentMatchers.anyString()))             doReturn(true)
 registrationForm.currentEmailAddress = "hilary@gmail.com"
        // then
        assertTrue(registrationForm.checkIfEmailCanBeRegistered())
 verify(view).showSuccessMessage("Email address is 
         available!")
 }
}
  1. 添加一个测试方法以验证如果地址不可用,是否会显示错误消息:
class MyTest {
    private val api = mock<RegistrationApi>()
    private val view = mock<TextView>()
    private var registrationForm = RegistrationForm(api, view)

    @Test
    fun `should display success message when email address is available`() {
        // given
        assertNotNull(registrationForm)
        // when we update the currentEmailAddress to any String
        whenever(api.isEmailAddressAvailable(ArgumentMatchers.anyString()))             doReturn(true)
        registrationForm.currentEmailAddress = "hilary@gmail.com"
        // then
        assertTrue(registrationForm.checkIfEmailCanBeRegistered())
        verify(view).showSuccessMessage("Email address is available!")
    }

    @Test
 fun `should display error message when email address isn't available`() {
 // given
        assertNotNull(registrationForm)
 // when
        registrationForm.currentEmailAddress = "hilary@gmail.com"
        whenever(api.isEmailAddressAvailable(ArgumentMatchers.anyString()))             doReturn(false)
 // then
        assertTrue(registrationForm.isEmailIsValid())
 verify(view).showErrorMessage(anyString())
 }
}

它是如何工作的...

除了行为模拟之外,Mockito Kotlin 提供了一种可靠的方式来验证在执行测试方法期间与模拟依赖项的交互。在should display success message when email address is availableshould display error message when email address isn't available函数中,我们只想检查TextView依赖项的期望功能是否被调用。为了做到这一点,我们调用verify()函数。例如,为了检查showErrorMessage()函数是否在模拟的view: TextView依赖项上被调用,我们调用以下代码:

verify(view).showErrorMessage(anyString())

如果showErrorMessage()没有被调用,测试方法将失败,并且适当的日志消息将被打印到控制台。

Kotlin 协程的单元测试

在这个菜谱中,我们将探讨如何有效地测试使用协程内部代码的代码。我们将编写一个单元测试,用于测试在尝试使用外部 API 验证给定用户凭据的同时,在后台异步运行的代码部分。我们将使用 Kotlin Mockito 库来模拟对外部 API 和TextCoroutineContext类的调用,从而能够轻松地测试异步代码。

准备工作

我们将使用 JUnit 库为运行测试用例类提供核心框架。我们需要通过在 gradle.build 脚本中声明它来将其添加到我们项目的项目依赖项列表中:

implementation group: 'junit', name: 'junit', version: '4.12'

为了使用 Kotlin Mockito 库,我们可以通过以下声明将其添加到项目依赖项中:

implementation 'com.nhaarman:mockito-kotlin:1.5.0'

您可以在 GitHub 仓库中找到的 AndroidSamples 项目中检查与 Android 框架相关的食谱的实现和配置:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook/。要遵循与 Android 相关的食谱,您只需在 Android Studio 中创建一个新的项目。

在这个食谱中,我们将为以下定义的 Authenticator 类编写一个单元测试:

class Authenticator(val api: Api) {

    fun tryToAuthorise(encodedUserNameAndPassword: ByteArray, 
                       context: CoroutineContext): Deferred<String> =
            async(context) {
                var authToken = api.authorise(encodedUserNameAndPassword)

                var retryCount = 0
                while (authToken.isEmpty() && retryCount <= 8) {
                    delay(10, TimeUnit.SECONDS)
                    authToken = api.authorise(encodedUserNameAndPassword)
                    retryCount++
                }

                authToken
            }
}

Api 属性如下所示:

interface Api {
    // returns a non-empty auth token when the given credentials were authorised
    fun authorise(encodedUserNameAndPassword: ByteArray): String
}

如何操作...

  1. 创建一个新的测试类:
class MyTest {
}
  1. 添加一个模拟的 Api 类型测试类属性:
class MyTest {
 val api: Api = mock()
}
  1. Authenticator 类实例化为类属性:
class MyTest {
    val api: Api = mock()
 val authenticator = Authenticator(api)
}
  1. 实现测试以验证在连续授权尝试失败的情况下,Api.authorise() 函数是否至少被调用 10 次:
class MyTest {
    val api: Api = mock()
    val authenticator = Authenticator(api)

    @Test
    fun `should retry auth at least 10 times when Api returns empty
    token`() {
 whenever(api.authorise(any())) doReturn ""

        val context = TestCoroutineContext()

 runBlocking(context) {
          authenticator.tryToAuthorise("admin:1234".toByteArray(),
            context)
 .await()
 context.advanceTimeBy(100, TimeUnit.SECONDS)
 verify(api, atLeast(10)).authorise(any())
 }
    }
}

它是如何工作的...

首先,通过模拟,我们避免了实现我们正在为其编写测试的 Authenticator 类的 Api 依赖。实际上,我们对测试 Api 实现返回的真实结果不感兴趣。我们想要测试 tryToAuthorise() 函数的机制,并验证在连续授权失败的情况下,它是否会至少重试调用 Api.authorise() 函数 10 次。这就是为什么我们将 api 模拟设置为对 authorise() 函数的结果始终返回空字符串的原因。

如您所想象,这样的测试将花费很多时间才能完成,因为内部,tryToAuthorise() 函数在重试授权之前会等待 10 秒。为了避免执行时间过长,我们需要通过将时间人为地向前推进 100 秒来检查 Api.authorise() 函数是否至少被调用 10 次。我们能够通过安排使用 runBlocking() 函数启动的两个协程,并在 tryToAuthorise() 函数内部,在同一个 TestCoroutineContext 实例上运行,来实现这一点。然后,为了将时间向前推进 100 秒,我们只需在 TestCoroutineContext 实例上调用 advanceTimeBy(100, TimeUnit.SECONDS) 函数。结果,我们的测试方法将在不到一秒内完成。

第九章:杂项

在本章中,我们将涵盖以下食谱:

  • Kotlin 和 Java 互操作性问题和解决方案

  • Kotlin 和 JavaScript 互操作性问题和解决方案

  • 生成类的重命名

  • 将 Kotlin 代码反编译为 JVM 字节码

  • 为导入添加自定义名称

  • 使用类型别名包装复杂类型声明

  • 表达式的try…catch声明

  • 安全的类型转换

简介

本章将专注于展示各种问题的实用解决方案,这些问题是 Kotlin 开发者在日常工作中遇到的。在这里,你可以找到有关与 Java 和 JavaScript 互操作性的问题和解决方案的有用提示,以及一些帮助你更有效地编写代码的巧妙技巧。

Kotlin 和 Java 互操作性

这个食谱将展示如何将 Java 和 Kotlin 类结合起来,并在同一应用程序组件中使用它们。我们将声明一个名为ColoredText的 Kotlin 数据类,它包含两种类型的StringColor属性。除了属性之外,它还将在伴随对象中暴露一个用于文本处理的实用函数。我们将学习如何使用这些属性,以及如何声明ColoredText类中的函数,使其在 Java 类中作为 JVM 静态方法可见。

如何做到...

  1. 声明ColoredText类:
data class ColoredText
@JvmOverloads
constructor(
        var text: String = "",
        var color: Color = defaultColor) {

    companion object {
        @JvmField
        val defaultColor = Color.BLUE
    }
}
  1. companion对象中实现一个静态 JVM 方法:
data class ColoredText
@JvmOverloads
constructor(
        var text: String = "",
        var color: Color = defaultColor) {

    companion object {
        @JvmField
        val defaultColor = Color.BLUE

 @JvmStatic
        fun processText(text: String): String =
 with(text) {
                    toLowerCase().trim().capitalize()
                }

    }

}
  1. 添加一个成员函数,允许你将text属性打印到控制台:
data class ColoredText
@JvmOverloads
constructor(
        var text: String = "",
        var color: Color = defaultColor) {

 fun printToConsole() = println(text)

    companion object {
        @JvmField
        val defaultColor = Color.BLUE

        @JvmStatic
        fun processText(text: String): String =
                with(text) {
                    toLowerCase().trim().capitalize()
                }
    }
}
  1. 实现一个使用 Kotlin 类函数和属性的 Java 类:
public class JavaApp {
    public static void main(String... args) {
        String rawText = 
              " one Of The Best Programming Skills You Can Have " +
              "Is Knowing When To Walk Away For Awhile. ";
        String text = ColoredText.processText(rawText);
        ColoredText myText =
              new ColoredText(text, ColoredText.defaultColor);
        myText.printToConsole();
    }
}

它是如何工作的...

结果,JavaApp Java 类中的主函数将打印以下智慧到控制台:

One of the best programming skills you can have is knowing when to walk away for awhile.

由于 Kotlin 和 Java 类都编译为包含在公共代码库中的相同 JVM 字节码,因此 Kotlin 和 Java 互操作性绝对无缝。然而,有一些特殊情况需要我们在将 Kotlin 声明以特定方式在 Java 侧可用时给予额外关注。

首先,在ColoredText类中,我们使用@JvmOverloads注解标记构造函数,这告诉编译器在声明任何默认属性值的情况下生成多个构造函数实例。多亏了这一点,我们可以在某些 Java 类中实例化ColoredText类,而无需传递text和/或color属性值。

接下来我们使用的注解是@JvmField,它告诉 Kotlin 编译器不要为这个属性生成 getter 和 setter 函数,并将其作为字段暴露。当通过 Kotlin 对象暴露一个常量值时,它提供了一个更简洁的语法来访问 Java 侧的值。

另一个常用的注解是 @JvmStatic。它的目的是告诉编译器需要为这个函数生成一个额外的静态方法,以便在 Java 中作为外部类的直接静态方法使用。例如,在我们的案例中,我们可以通过以下方式在 Java 中访问 processText() 函数,省略 Companion 元素:

ColoredText.processText("sample text")

Kotlin 和 JavaScript 互操作性

在下面的菜谱中,我们将配置并实现一个示例 Web 应用程序项目,以探索 Kotlin 如何编译成 JavaScript。我们将实现一个简单的 Web 应用程序,当应用程序启动时将打开一个警告对话框。以下示例将展示如何将 Kotlin 和 JavaScript 代码结合在一起,并使用 Gradle 构建脚本配置 JavaScript 编译。

准备工作

为了设置项目以将 Kotlin 文件编译成 JavaScript,我们需要在模块级别的 Gradle 构建脚本中添加以下属性。首先,我们需要应用 Kotlin2Js 插件。我们可以通过以下声明来完成:

apply plugin: "kotlin2js"

到目前为止,每次我们执行 Gradle 的 build 任务时,Kotlin2JS 编译器都会生成来自 Kotlin 文件的相应函数和类,并将它们写入 build/classes/kotlin/ 目录下的 JS 文件,该文件以项目名称命名。然而,我们可以通过指定输出文件参数来修改此默认行为:

compileKotlin2Js.kotlinOptions.outputFile = "${projectDir}/web/js/app.js"

因此,Kotlin 文件编译的输出将位于 web/js 目录 下的 app.js 文件中。

然而,为了执行转换后的 Kotlin 代码,我们还需要将其与 Kotlin JS 标准库链接起来。我们可以修改 Gradle 构建脚本,将所需的库包含在 web/js 输出目录中:

build.doLast {
    configurations.compile.each { File file ->
        copy {
            includeEmptyDirs = false

            from zipTree(file.absolutePath)
            into "${projectDir}/web/js/lib"
            include { fileTreeElement ->
                def path = fileTreeElement.path
                path.endsWith(".js") && (path.startsWith("META-INF/resources/") || !path.startsWith("META-INF/"))
            }
        }
    }
}

您可以在示例项目中检查 Kotlin2JS 插件的配置:github.com/PacktPublishing/Kotlin-Standard-Library-Cookbook/tree/master/Kotlin-Samples-JS.

如何做到这一点...

  1. 创建一个新的 Kotlin 文件,AlertDialogApp.kt,其中包含 main() 函数:
fun main(args : Array<String>) {}
  1. 声明对 JS alert() 函数的引用:
fun main(args : Array<String>) {}

external fun alert(message: Any?): Unit
  1. 实现一个 showAlert() 函数并在 main() 函数中调用它:
fun main(args : Array<String>) {
    showAlert()
}

fun showAlert() {
 val number: dynamic = js("Math.floor(Math.random() * 1000)")
 val message = "There were $number viruses found on your computer! \uD83D\uDE31"
    println("showing alert")
 alert(message)
}

external fun alert(message: Any?): Unit

它是如何工作的...

如您所见,在运行 Gradle 构建任务后,AlertDialogApp.kt 文件将被转换为 app.js JavaScript 代码,位于 web/js 目录下,以及链接在 web/js/lib 目录下的 kotlin.js 文件。

我们可以通过在网页浏览器中运行它来测试生成的 JS 代码。为了做到这一点,我们将在项目的主目录下创建一个名为 test_app.html 的示例 HTML 文件,该文件将链接 kotlin.js 标准库文件并运行包含从 AlertDialogApp.kt 文件生成的 main() 函数实现的 app.js 文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
    <script src="img/kotlin.js"></script>
    <script src="img/app.js"></script>
</body>
</html>

结果,当我们在一个网络浏览器中打开 test_app.html 文件时,我们将遇到以下弹出对话框:

图片

AlertDialogApp.kt 文件中的 showAlert() 函数内部,我们使用 JavaScript 的 Math.floor()Math.random() 函数生成一个 0-1,000 之间的随机整数。我们使用 Kotlin 标准库中可用的 js() 函数在 Kotlin 代码中内联 JS 代码。正如你所看到的,js() 返回的结果被声明为 dynamic 类型。

dynamic 修饰符用于声明动态类型,这是每个 JavaScript 对象的特征。在 Kotlin 代码中,它可以作为强类型声明的替代品使用。当我们处理可能返回未指定类型结果的第三方 JS 库时,使用它是有意义的。

接下来,我们使用通过内联的 JS 代码生成的随机整数来组合将要显示的消息。最后,我们调用 JS 的 alert() 函数,并将组合的消息传递给它。这次,我们使用了 external 修饰符,它告诉 Kotlin 编译器相应的声明是用纯 JavaScript 编写的,它不应该为它生成实现。

还有更多...

你可能会想知道是否有实际的方法可以在基于 Gradle 的项目中处理 npm JS 依赖项。有一个解决方案允许你轻松地将你的 Kotlin 项目与 npm 依赖项集成,称为 Kotlin Frontend Gradle 插件。你可以在官方项目指南中了解更多信息:github.com/Kotlin/kotlin-frontend-plugin

重命名生成的函数

在这个菜谱中,我们将学习如何在 Kotlin 函数被编译成生成的 JVM 字节码时修改其名称。我们需要这个功能是因为在生成 JVM 字节码时会发生类型擦除。然而,多亏了 @JvmName 注解,我们可以声明多个不同的函数,但它们具有相同的名称,并在 Kotlin 代码中使用它们的原始名称,同时保持它们的 JVM 字节码名称不同,以满足编译器的要求。

如何做到这一点...

  1. 声明两个具有相同名称的函数:
fun List<String>.join(): String {
    return joinToString()
}

fun List<Int>.join(): String =
    map { it.toString() }
            .joinToString()    
  1. 使用适当的注解标记函数:
@JvmName("joinStringList")
fun List<String>.join(): String {
    return joinToString()
}

@JvmName("joinIntList")
fun List<Int>.join(): String =
    map { it.toString() }
            .joinToString()

它是如何工作的...

由于提供了替代函数名,我们能够将它们编译成 JVM 字节码。然而,你可以轻松地测试我们可以在 Kotlin 代码中使用它们的原始名称。这是因为 Kotlin 编译器能够根据它们的返回类型和泛型类型参数值正确地识别它们。

你可以通过在整数列表和字符串列表上运行 join() 函数来测试这一点:

fun main(vararg args: String) {
    println(listOf(1, 2, 3).join())
    println(listOf("a", "b", "c").join())
}

结果,前面的代码将打印以下文本到控制台:

1, 2, 3
a, b, c

请记住,当你想要从 Java 调用这些函数时,你需要使用它们的替代名称:joinStringList()joinIntList()

还有更多...

此外,还有一个相应的 @JsName 注解,它允许你更改 JavaScript 函数和类的名称。如果你使用 Kotlin2JS 插件将 Kotlin 文件编译到 JavaScript,你可以使用它。如果你想熟悉 Kotlin2JS 插件的基础知识,你可以检查 Kotlin 和 JavaScript 互操作性 食谱。

参见

  • 如果你想要了解如何探索从 Kotlin 文件生成的最终 JVM 字节码,请阅读 反编译 Kotlin 代码到 Java 和 JVM 字节码 食谱

反编译 Kotlin 代码到 Java 和 JVM 字节码

在本食谱中,我们将学习如何轻松地将 Kotlin 文件反编译,以查看其对应的 JVM 字节码是如何实现的,以及字节码对应的 Java 实现看起来会是什么样子。这可以帮助你发现 Kotlin 各种概念在底层是如何实现的。这也有助于代码调试和优化。

准备工作

让我们创建一个新的 Kotlin 文件,命名为 Recipe4.kt,其中包含以下示例实现,以便查看其字节码转换:

data class A(val a: String = "a") {
    companion object {
        @JvmStatic
        fun foo(): String = "Wooo!"
    }    
}

如何操作...

  1. 在 IntelliJ 中打开 Recipe4.kt 文件。

  2. 从工具/ Kotlin 菜单中选择“显示 Kotlin 字节码”选项。框将显示 JVM 字节码实现。

  3. 点击 Kotlin 字节码视图中的“反编译”按钮。相应的 Java 实现将从字节码中反编译出来,并出现在新窗口中。

它是如何工作的...

分析为 data class A 生成的 Java 实现的任务留作读者练习。你可以通过从 Kotlin 类定义中移除 data 修饰符来实验,并观察字节码和反编译的 Java 实现中的变化。

为导入添加自定义名称

在本食谱中,我们将探讨如何为 import 声明添加自定义名称。我们将导入 java.lang.StringBuilder 类,为其添加一个自定义名称,并在示例代码中使用它来演示其用法。

如何操作...

  1. 使用自定义别名导入 StringBuilder 类:
import java.lang.StringBuilder as builder
  1. 在示例代码中使用自定义的 StringBuilder 名称:
import java.lang.StringBuilder as builder

fun main(vararg args: String) {
 val text = builder()
 .append("Code is like humor. ")
 .append("When you have to explain it, ")
 .append("it’s bad.")
 .toString()
 println(text)
}

它是如何工作的...

如你所见,我们能够使用替代名称而不是 StringBuilder 类。这是一个小功能,但有时可以用来使你的代码更容易阅读。我们的示例代码将打印以下文本到控制台:

Code is like humor. When you have to explain it, it’s bad.

使用类型别名包装复杂类型声明

有时我们需要处理长或冗长的类型声明。幸运的是,在 Kotlin 中,我们能够为任何现有类型分配一个替代名称,并使用较短的替代名称。这也可以帮助你编写更易于理解的优雅代码。本食谱将演示如何使用类型别名。

准备工作

假设我们有两个预定义的类:

data class Song(val title: String)
data class Artist(val name: String)

我们将定义一个Song类型值的映射类型别名和一个泛型键类型–Map<T, List<Song>>。接下来,我们将使用它来定义一个函数,该函数将返回给定Map<Artist, List<Song>>对象的最受欢迎的Artist实例。

如何实现...

  1. 声明一个Map<T, List<Song>>类型的泛型类型别名:
typealias GrouppedSongs<T> = Map<T, List<Song>>
  1. 使用类型别名实现getMostPopularArtist()函数:
fun getMostPopularArtist(songs: GrouppedSongs<Artist>) =
    songs.toList().sortedByDescending {it.second.size }.first().first

它是如何工作的...

使用类型别名,我们能够为类型提供一个自定义名称,并且可以在getMostPopularArtist(songs: GrouppedSongs<Artist>)中使用它,而不是使用Map<Artist, List<Song>>类型,这导致了一个更有意义的声明。我们可以通过用示例数据调用getMostPopularArtist()来测试我们的实现:

val songs: GrouppedSongs<Artist> =
        mapOf(
                Artist("Bob Dylan") to
                        listOf(Song("Blowing In The Wind"),
                               Song("To Fall in Love With You")),

                Artist("Louis Armstrong") to
                        listOf(Song("What A Beautiful World"))
        )

println("${getMostPopularArtist(songs)} is the most popular")

因此,我们将得到以下文本打印到控制台:

Artist(name=Bob Dylan) is most popular

表达式的 try…catch 声明

Kotlin 被宣传为一种极其表达性的语言。然而,这是语言的一个特点,在开始时可能并不明显,尤其是如果你习惯了像 Java 或 JavaScript 这样的其他语言。为了更清楚地展示语言风格,在这个菜谱中,我们将通过将其视为表达式来发现如何在 Kotlin 中以try…catch声明的方式工作。

准备工作

让我们考虑以下 Java 代码:

int value;
try {
    result = parseInt(input);
} catch (NumberFormatException e) {
} finally {
    result = 0;
}

它声明了一个int类型的result变量。接下来,它尝试使用Integer.parseInt()函数将字符串值解析为整数,如果成功,则将结果赋值给value变量。如果parseInt()无法解析字符串,则将默认值0赋值给value变量。

如何实现...

  1. try…catch声明中调用parseInt()函数:
try {
    parseInt("fdsaa")
} catch (e: NumberFormatException) {
    0
}
  1. try…catch声明的结果赋值给value变量:
val result = try {
    parseInt("fdsaa")
} catch (e: NumberFormatException) {
    0
}

它是如何工作的...

就这样。Kotlin 中的try…catch声明可以被赋值给变量。原因是它实际上是一个表达式!在我们的例子中,try…catch返回parseInt()函数的结果,当函数抛出异常时,它返回0

更多...

类似地,我们可以将其他语言声明视为表达式。将控制流语句(如ifwhen)返回的值赋给变量是一种常见的做法。例如,我们可以以下这种方式使用when作为表达式:

val result = when(input) {
    is Int -> input
    is String -> parseInt(input)
    else -> 0
}

安全的类型转换

无论何时进行类型转换,我们都应该牢记这是一个潜在的异常来源。这就是为什么我们应该始终使用is修饰符进行类型检查,或者在try…catch块中进行转换的原因。然而,在 Kotlin 中,我们还有一个安全的转换选项,它不会抛出ClassCastException,而是返回null。在这个菜谱中,我们将测试安全转换的实际应用。

如何实现...

  1. 让我们先定义一个返回随机Double值的Number类型的函数:
fun getRandomNumber(): Number = Random().nextDouble() * 10
  1. 尝试使用安全转换操作符将函数的结果转换为不同的类型,并将转换后的值打印到控制台:
println(getRandomNumber() as? Int)
println(getRandomNumber() as? Double)
println(getRandomNumber() as? String)

它是如何工作的...

上述代码不会失败也不会抛出任何异常。它只会返回 null 值。我们的转换测试代码将打印以下输出到控制台:

null
8.802117014997226
null

使用 as? 转换修饰符是传统方式的整洁替代品。如果你正在处理可空类型,即在与不提供空安全的外部库一起工作时,可以使用它。然而,如果你能从空安全中受益,最好使用标准的转换操作。

posted @ 2025-10-09 13:23  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报