泛型、对象和扩展

1. 简介

几十年来,程序员设计了几种编程语言特性来帮助您编写更好的代码——用更少的代码表达相同的想法,抽象表达复杂的想法,以及编写防止其他开发人员意外出错的代码,这仅仅是几个例子。Kotlin 语言也不例外,它有一些特性旨在帮助开发人员编写更具表现力的代码。

不幸的是,如果您是第一次编程,这些特性可能会使事情变得棘手。虽然它们听起来很有用,但它们的用途和解决的问题可能并不总是显而易见的。您可能已经在 Compose 和其他库中看到了一些特性的使用。

虽然没有经验的替代品,但此代码实验室让您接触了几个 Kotlin 概念,这些概念可以帮助您构建更大的应用程序

  • 泛型
  • 不同类型的类(枚举类和数据类)
  • 单例和伴生对象
  • 扩展属性和函数
  • 作用域函数

在本代码实验室结束时,您应该对在本课程中已经看到的代码有更深入的了解,并学习一些在您自己的应用程序中遇到或使用这些概念的示例。

先决条件

  • 熟悉面向对象编程概念,包括继承。
  • 如何定义和实现接口。

您将学到什么

  • 如何为类定义泛型类型参数。
  • 如何实例化泛型类。
  • 何时使用枚举类和数据类。
  • 如何定义必须实现接口的泛型类型参数。
  • 如何使用作用域函数访问类属性和方法。
  • 如何为类定义单例对象和伴生对象。
  • 如何使用新属性和方法扩展现有类。

您需要什么

  • 可以访问 Kotlin Playground 的 Web 浏览器。

2. 使用泛型创建可重用类

假设您正在为在线测验编写一个应用程序,类似于您在本课程中看到的测验。通常有多种类型的测验问题,例如填空题或判断题。单个测验问题可以用一个类来表示,该类包含几个属性。

测验中的问题文本可以用字符串表示。测验问题还需要表示答案。但是,不同的问题类型(例如判断题)可能需要使用不同的数据类型来表示答案。让我们定义三种不同类型的问题。

  • 填空题:答案是一个用String表示的单词。
  • 判断题:答案用Boolean表示。
  • 数学题:答案是一个数值。简单算术题的答案用Int表示。

此外,我们示例中的测验问题,无论问题类型如何,还将具有难度评级。难度评级用字符串表示,有三个可能的值:"easy""medium""hard"

定义类以表示每种类型的测验问题

  1. 导航到Kotlin Playground
  2. main()函数上方,为填空题定义一个名为FillInTheBlankQuestion的类,该类包含一个用于questionTextString属性、一个用于answerString属性和一个用于difficultyString属性。
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)
  1. FillInTheBlankQuestion类下方,定义另一个名为TrueOrFalseQuestion的类,用于判断题,该类包含一个用于questionTextString属性、一个用于answerBoolean属性和一个用于difficultyString属性。
class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
  1. 最后,在其他两个类下方,定义一个NumericQuestion类,该类包含一个用于questionTextString属性、一个用于answerInt属性和一个用于difficultyString属性。
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)
  1. 看看您编写的代码。您是否注意到重复的部分?
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)

class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)

所有三个类都具有完全相同的属性:questionTextanswerdifficulty。唯一的区别是answer属性的数据类型。您可能会认为显而易见的解决方案是创建一个具有questionTextdifficulty的父类,每个子类都定义answer属性。

但是,使用继承与上面相同的问题。每次添加新的问题类型时,都必须添加answer属性。唯一的区别是数据类型。拥有一个没有答案属性的父类Question看起来也很奇怪。

当您希望属性具有不同的数据类型时,子类化不是答案。相反,Kotlin 提供了名为泛型的内容,允许您拥有一个可以具有不同数据类型的单个属性,具体取决于特定用例。

什么是泛型数据类型?

泛型类型,或简称泛型,允许数据类型(例如类)指定未知的占位符数据类型,该数据类型可以与其属性和方法一起使用。这到底是什么意思?

在上面的示例中,您可以创建一个表示任何问题的单个类,并为answer属性的数据类型使用占位符名称,而不是为每种可能的数据类型定义一个答案属性。实际的数据类型——StringIntBoolean等——是在实例化该类时指定的。在使用占位符名称的任何地方,都会使用传递到类中的数据类型。定义类泛型类型的语法如下所示

67367d9308c171da.png

实例化类时提供泛型数据类型,因此需要将其定义为类签名的一部分。类名后面是一个左尖括号(<),后跟数据类型的占位符名称,再后跟一个右尖括号(>)。

然后,可以在类中使用实际数据类型的任何地方使用占位符名称,例如属性。

81170899b2ca0dc9.png

这与任何其他属性声明相同,只是使用占位符名称而不是数据类型。

您的类最终如何知道使用哪种数据类型?实例化类时,泛型类型作为参数以尖括号的形式传递。

9b8fce54cac8d1ea.png

类名后面是一个左尖括号(<),后跟实际数据类型StringBooleanInt等,再后跟一个右括号(>)。您为泛型属性传递的值的数据类型必须与尖括号中的数据类型匹配。您将使 answer 属性成为泛型,以便您可以使用一个类来表示任何类型的测验问题,无论答案是StringBooleanInt还是任何任意数据类型。

重构代码以使用泛型

重构您的代码以使用名为Question的单个类,该类具有泛型答案属性。

  1. 删除FillInTheBlankQuestionTrueOrFalseQuestionNumericQuestion的类定义。
  2. 创建一个名为Question的新类。
class Question()
  1. 在类名之后,但在括号之前,使用左尖括号和右尖括号添加泛型类型参数。将泛型类型称为T
class Question<T>()
  1. 添加questionTextanswerdifficulty属性。questionText应为String类型。answer应为T类型,因为其数据类型是在实例化Question类时指定的。difficulty属性应为String类型。
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: String
)
  1. 要查看如何在多种问题类型(填空题、判断题等)中使用此功能,请在main()中创建三个Question类的实例,如下所示。
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", "medium")
    val question2 = Question<Boolean>("The sky is green. True or false", false, "easy")
    val question3 = Question<Int>("How many days are there between full moons?", 28, "hard")
}
  1. 运行你的代码以确保一切正常。你现在应该有三个Question类的实例——每个实例都使用不同的数据类型作为答案——而不是三个不同的类,或者不是使用继承。如果你想处理具有不同答案类型的题目,你可以重用相同的Question类。

3. 使用枚举类

在上一节中,你定义了一个难度属性,它有三个可能的值:“简单”、“中等”和“困难”。虽然这可以工作,但存在几个问题。

  1. 如果你不小心错打这三个可能字符串中的一个,可能会引入错误。
  2. 如果值发生变化,例如,"medium"重命名为"average",则需要更新字符串的所有用法。
  3. 没有什么可以阻止你或其他开发人员意外地使用不是这三个有效值之一的其他字符串。
  4. 如果你添加更多难度级别,代码将更难维护。

Kotlin 帮助你使用一种称为枚举类的特殊类来解决这些问题。枚举类用于创建具有有限可能值的类型。例如,在现实世界中,四个基点——北、南、东和西——可以用枚举类表示。不需要,并且代码不应该允许使用任何其他方向。枚举类的语法如下所示。

f4bddb215eb52392.png

枚举的每个可能值称为枚举常量。枚举常量放置在花括号内,用逗号分隔。约定是在常量名称中将每个字母都大写。

你可以使用点运算符引用枚举常量。

f3cfa84c3f34392b.png

使用枚举常量

修改你的代码以使用枚举常量而不是String来表示难度。

  1. Question类下面,定义一个名为Difficultyenum类。
enum class Difficulty {
    EASY, MEDIUM, HARD
}
  1. Question类中,将difficulty属性的数据类型从String更改为Difficulty
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. 初始化三个问题时,传入难度的枚举常量。
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

4. 使用数据类

你到目前为止使用过的许多类,例如Activity的子类,都有几个方法来执行不同的操作。这些类不仅表示数据,还包含许多功能。

另一方面,像Question这样的类只包含数据。它们没有任何执行操作的方法。这些可以定义为数据类。将类定义为数据类允许 Kotlin 编译器做出某些假设,并自动实现一些方法。例如,toString()println()函数在后台调用。当你使用数据类时,toString()和其他方法将根据类的属性自动实现。

要定义数据类,只需在class关键字之前添加data关键字。

e7cd946b4ad216f4.png

Question转换为数据类

首先,你将看到当你尝试在不是数据类的类上调用像toString()这样的方法时会发生什么。然后,你将Question转换为数据类,以便默认实现此方法和其他方法。

  1. main()中,打印在question1上调用toString()的结果。
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
    println(question1.toString())
}
  1. 运行你的代码。输出仅显示类名和对象的唯一标识符。
Question@37f8bb67
  1. 使用data关键字将Question转换为数据类。
data class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. 再次运行你的代码。通过将其标记为数据类,Kotlin 能够确定在调用toString()时如何显示类的属性。
Question(questionText=Quoth the raven ___, answer=nevermore, difficulty=MEDIUM)

当类定义为数据类时,将实现以下方法。

  • equals()
  • hashCode():在使用某些集合类型时,你会看到此方法。
  • toString()
  • componentN()component1()component2(),等等。
  • copy()

5. 使用单例对象

在许多情况下,你希望一个类只有一个实例。例如

  1. 移动游戏中当前用户的玩家统计数据。
  2. 与单个硬件设备交互,例如通过扬声器发送音频。
  3. 访问远程数据源(例如 Firebase 数据库)的对象。
  4. 身份验证,其中一次只能登录一个用户。

在上述场景中,你可能需要使用一个类。但是,你只需要实例化该类的一个实例。如果只有一个硬件设备,或者一次只有一个用户登录,则没有理由创建多个实例。拥有两个同时访问相同硬件设备的对象可能会导致一些非常奇怪和有错误的行为。

你可以通过将对象定义为单例来清楚地在代码中传达对象应该只有一个实例。单例是一个只能有一个实例的类。Kotlin 提供了一个特殊的构造,称为对象,可用于创建单例类。

定义一个单例对象

645e8e8bbffbb5f9.png

对象的语法类似于类的语法。只需使用object关键字而不是class关键字。单例对象不能有构造函数,因为你不能直接创建实例。相反,所有属性都在花括号内定义,并被赋予初始值。

前面给出的一些示例可能看起来并不明显,特别是如果你还没有使用特定的硬件设备或在你的应用中处理身份验证。但是,随着你继续学习 Android 开发,你会看到单例对象出现。让我们用一个简单的示例来演示它,该示例使用一个用于用户状态的对象,其中只需要一个实例。

对于测验,最好有一种方法来跟踪题目的总数以及学生到目前为止回答的题目数。你只需要存在此类的一个实例,因此,不要将其声明为类,而是将其声明为单例对象。

  1. 创建一个名为StudentProgress的对象。
object StudentProgress {
}
  1. 在此示例中,我们将假设总共有 10 道题,其中 3 道题已回答。添加两个Int属性:值为10total和值为3answered
object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}

访问单例对象

还记得你不能直接创建单例对象的实例吗?那么你如何访问它的属性呢?

因为一次只有一个StudentProgress实例存在,所以你可以通过引用对象本身的名称,后跟点运算符(.),后跟属性名称来访问其属性。

1b610fd87e99fe25.png

更新你的main()函数以访问单例对象的属性。

  1. main()中,添加对println()的调用,该调用输出StudentProgress对象中的answeredtotal问题。
fun main() {
    ...
    println("${StudentProgress.answered} of ${StudentProgress.total} answered.")
}
  1. 运行你的代码以验证一切正常。
...
3 of 10 answered.

将对象声明为伴生对象

Kotlin 中的类和对象可以在其他类型中定义,并且可以成为组织代码的好方法。你可以使用伴生对象在另一个类中定义单例对象。伴生对象允许你从类内部访问其属性和方法,如果对象的属性和方法属于该类,则允许使用更简洁的语法。

要声明一个伴生对象,只需在object关键字之前添加companion关键字。

68b263904ec55f29.png

你将创建一个名为Quiz的新类来存储测验题目,并将StudentProgress设为Quiz类的伴生对象。

  1. Difficulty枚举下面,定义一个名为Quiz的新类。
class Quiz {
}
  1. question1question2question3main()移动到Quiz类中。如果还没有,你还需要删除println(question1.toString())
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

}
  1. StudentProgress对象移动到Quiz类中。
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

    object StudentProgress {
        var total: Int = 10
        var answered: Int = 3
    }
}
  1. 使用companion关键字标记StudentProgress对象。
companion object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}
  1. 更新对println()的调用,以使用Quiz.answeredQuiz.total引用属性。即使这些属性是在StudentProgress对象中声明的,但也可以使用点表示法仅使用Quiz类的名称来访问它们。
fun main() {
    println("${Quiz.answered} of ${Quiz.total} answered.")
}
  1. 运行你的代码以验证输出。
3 of 10 answered.

6. 使用新属性和方法扩展类

在使用 Compose 时,你可能已经注意到在指定 UI 元素大小时的一些有趣的语法。数值类型,例如 Double,似乎具有 dpsp 等属性来指定尺寸。

a25c5a0d7bb92b60.png

为什么 Kotlin 语言的设计者会在内置数据类型上包含属性和函数,特别是用于构建 Android UI?他们能够预测未来吗?Kotlin 是否在 Compose 存在之前就被设计用于与 Compose 一起使用?

当然不是!当你编写一个类时,你通常并不知道另一个开发人员将在他们的应用程序中如何使用它,或者计划如何使用它。不可能预测所有未来的用例,也不明智地为了某些不可预见的用例而向你的代码添加不必要的膨胀。

Kotlin 语言所做的是,赋予其他开发人员扩展现有数据类型的能力,添加可以使用点语法访问的属性和方法,就好像它们是该数据类型的一部分一样。例如,一个没有参与 Kotlin 中浮点数类型开发的开发人员,比如构建 Compose 库的人,可以选择添加特定于 UI 尺寸的属性和方法。

既然你在学习前两个单元的 Compose 时已经看到了这种语法,那么现在是时候学习它在底层是如何工作的了。你将添加一些属性和方法来扩展现有类型。

添加扩展属性

要定义扩展属性,请在变量名前添加类型名称和点运算符 (.)。

1e8a52e327fe3f45.png

你将重构 main() 函数中的代码,以使用扩展属性打印测验进度。

  1. Quiz 类下方,定义一个名为 progressTextQuiz.StudentProgress 扩展属性,类型为 String
val Quiz.StudentProgress.progressText: String
  1. 为扩展属性定义一个 getter,它返回之前在 main() 中使用的相同字符串。
val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. 用打印 progressText 的代码替换 main() 函数中的代码。因为这是一个伴生对象的扩展属性,所以你可以使用类的名称 Quiz 通过点表示法访问它。
fun main() {
    println(Quiz.progressText)
}
  1. 运行你的代码以验证它是否有效。
3 of 10 answered.

添加扩展函数

要定义扩展函数,请在函数名前添加类型名称和点运算符 (.)。

879ff2761e04edd9.png

你将添加一个扩展函数以将测验进度输出为进度条。由于你实际上无法在 Kotlin 游乐场中创建进度条,因此你将使用文本打印出复古风格的进度条!

  1. StudentProgress 对象添加一个名为 printProgressBar() 的扩展函数。该函数不应接受任何参数,也不应返回值。
fun Quiz.StudentProgress.printProgressBar() {
}
  1. 打印 字符 answered 次,使用 repeat()。进度条的这部分深色阴影表示已回答的问题数量。使用 print(),因为你不想在每个字符之后换行。
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
}
  1. 打印 字符,次数等于 totalanswered 之差,使用 repeat()。这部分浅色阴影表示进度条中剩余的问题。
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
}
  1. 使用 println() 打印一个新行,不带任何参数,然后打印 progressText
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. 更新 main() 中的代码以调用 printProgressBar()
fun main() {
    Quiz.printProgressBar()
}
  1. 运行你的代码以验证输出。
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

是否必须执行其中任何一项?当然不是。但是,拥有扩展属性和方法的选项为你提供了更多向其他开发人员公开代码的选项。在其他类型上使用点语法可以使你的代码更易于阅读,无论对你还是对其他开发人员而言。

7. 使用接口重写扩展函数

在上一页中,你看到了如何在不直接向 StudentProgress 对象添加代码的情况下,使用扩展属性和扩展函数向其添加属性和方法。虽然这是向已定义的一个类添加功能的好方法,但如果你有权访问源代码,则并不总是需要扩展类。还有一些情况是你不知道实现应该是什么,只知道应该存在某个方法或属性。如果你需要多个类具有相同的附加属性和方法,也许行为不同,则可以使用接口定义这些属性和方法。

例如,除了测验之外,假设你还有用于调查、食谱步骤或任何其他可以使用进度条的有序数据的类。你可以定义一个称为接口的东西,它指定每个这些类必须包含的方法和/或属性。

eeed58ed687897be.png

接口使用 interface 关键字定义,后跟一个 UpperCamelCase 命名,然后是花括号。在花括号内,你可以定义任何方法签名或任何符合接口的类必须实现的只读属性。

6b04a8f50b11f2eb.png

接口是一种契约。符合接口的类被称为扩展接口。类可以使用冒号 (:) 后跟一个空格,然后是接口的名称来声明它希望扩展接口。

78af59840c74fa08.png

作为回报,该类必须实现接口中指定的所有属性和方法。这使你可以轻松地确保任何需要扩展接口的类都以完全相同的方法签名实现完全相同的方法。如果你以任何方式修改接口,例如添加或删除属性或方法或更改方法签名,编译器要求你更新任何扩展接口的类,从而使你的代码保持一致并更容易维护。

接口允许扩展它们的类的行为发生变化。每个类负责提供实现。

让我们看看如何重写进度条以使用接口,并使 Quiz 类扩展该接口。

  1. Quiz 类上方,定义一个名为 ProgressPrintable 的接口。我们选择名称 ProgressPrintable,因为它使任何扩展它的类都能够打印进度条。
interface ProgressPrintable {
}
  1. ProgressPrintable 接口中,定义一个名为 progressText 的属性。
interface ProgressPrintable {
    val progressText: String
}
  1. 修改 Quiz 类的声明以扩展 ProgressPrintable 接口。
class Quiz : ProgressPrintable {
    ... 
}
  1. Quiz 类中,添加一个名为 progressText 的类型为 String 的属性,如 ProgressPrintable 接口中所指定。由于该属性来自 ProgressPrintable,因此在 val 前面加上 override 关键字。
override val progressText: String
  1. 复制旧 progressText 扩展属性的属性 getter。
override val progressText: String
        get() = "${answered} of ${total} answered"
  1. 删除旧的 progressText 扩展属性。

要删除的代码

val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. ProgressPrintable 接口中,添加一个名为 printProgressBar 的方法,该方法不接受任何参数,也不返回值。
interface ProgressPrintable {
    val progressText: String
    fun printProgressBar()
}
  1. Quiz 类中,使用 override 关键字添加 printProgressBar() 方法。
override fun printProgressBar() {
}
  1. 将旧 printProgressBar() 扩展函数中的代码移动到接口中的新 printProgressBar() 中。修改最后一行以引用接口中的新 progressText 变量,方法是删除对 Quiz 的引用。
override fun printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(progressText)
}
  1. 删除扩展函数 printProgressBar()。此功能现在属于扩展 ProgressPrintableQuiz 类。

要删除的代码

fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. 更新 main() 中的代码。由于 printProgressBar() 函数现在是 Quiz 类的方法,因此你需要首先实例化一个 Quiz 对象,然后调用 printProgressBar()
fun main() {
    Quiz().printProgressBar()
}
  1. 运行你的代码。输出没有改变,但你的代码现在更模块化了。随着代码库的增长,你可以轻松地添加符合相同接口的类以重用代码,而无需从超类继承。
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

接口有许多用例可以帮助你构建代码,并且你将在通用单元中经常看到它们的使用。以下是一些你继续使用 Kotlin 时可能会遇到的接口示例。

  • 手动依赖注入。创建一个接口,定义依赖项的所有属性和方法。将接口作为依赖项(活动、测试用例等)的数据类型,以便可以使用实现接口的任何类的实例。这允许你交换底层实现。
  • 用于自动化测试的模拟。模拟类和真实类都符合相同的接口。
  • Compose Multiplatform 应用程序中访问相同的依赖项。例如,创建一个接口,为 Android 和桌面提供一组通用的属性和方法,即使每个平台的底层实现不同。
  • Compose 中的几个数据类型,例如 Modifier,都是接口。这允许你添加新的修饰符,而无需访问或修改底层源代码。

8. 使用作用域函数访问类属性和方法

正如你已经看到的,Kotlin 包含了许多功能来使你的代码更简洁。

在你继续学习 Android 开发的过程中,你会遇到的一项这样的功能是作用域函数。作用域函数允许你简洁地访问类的属性和方法,而无需重复访问变量名称。这到底意味着什么?让我们来看一个例子。

使用作用域函数消除重复的对象引用

作用域函数是高阶函数,允许你访问对象的属性和方法,而无需引用对象的名称。它们被称为作用域函数,因为传递进去的函数体采用了调用作用域函数的对象的作用域。例如,一些作用域函数允许你访问类中的属性和方法,就好像这些函数是在该类中定义的方法一样。这可以通过让你在包含冗余时省略对象名称来使你的代码更具可读性。

为了更好地说明这一点,让我们看看在本课程后面你会遇到的一些不同的作用域函数。

使用 let() 替换长的对象名称

let() 函数允许你在 lambda 表达式中使用标识符 it 来引用对象,而不是对象的实际名称。这可以帮助你避免在多次访问多个属性时重复使用长而描述性的对象名称。let() 函数是一个扩展函数,可以使用点表示法在任何 Kotlin 对象上调用。

尝试使用 let() 访问 question1question2question3 的属性

  1. Quiz 类添加一个名为 printQuiz() 的函数。

fun printQuiz() {
    
}
  1. 添加以下代码,该代码打印问题的questionTextanswerdifficulty。虽然对question1question2question3访问了多个属性,但每次都使用了整个变量名。如果变量名发生了更改,则需要更新每个用法。
fun printQuiz() {
    println(question1.questionText)
    println(question1.answer)
    println(question1.difficulty)
    println()
    println(question2.questionText)
    println(question2.answer)
    println(question2.difficulty)
    println()
    println(question3.questionText)
    println(question3.answer)
    println(question3.difficulty)
    println()
}
  1. 将访问questionTextanswerdifficulty属性的代码用对question1question2question3let()函数调用包围起来。在每个 lambda 表达式中用它替换变量名。
fun printQuiz() {
    question1.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question2.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question3.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
}
  1. 更新main()中的代码,以创建名为quizQuiz类的实例。
fun main() {
    val quiz = Quiz()
}
  1. 调用printQuiz()
fun main() {
    val quiz = Quiz()
    quiz.printQuiz()
}
  1. 运行你的代码以验证一切正常。
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

使用 apply() 在没有变量的情况下调用对象的方法

作用域函数的一个很酷的功能是,您可以在对象甚至分配给变量之前就在该对象上调用它们。例如,apply()函数是一个扩展函数,可以使用点表示法在对象上调用。 apply()函数还返回对该对象的引用,以便将其存储在变量中。

更新main()中的代码以调用apply()函数。

  1. 在创建Quiz类的实例时,在右括号后调用apply()。调用apply()时可以省略括号,并使用尾随 lambda 语法。
val quiz = Quiz().apply {
}
  1. 将对printQuiz()的调用移动到 lambda 表达式内部。您不再需要引用quiz变量或使用点表示法。
val quiz = Quiz().apply {
    printQuiz()
}
  1. apply()函数返回Quiz类的实例,但由于您不再在任何地方使用它,因此删除quiz变量。使用apply()函数,您甚至不需要变量就可以在Quiz的实例上调用方法。
Quiz().apply {
    printQuiz()
}
  1. 运行您的代码。请注意,您能够在没有对Quiz实例的引用的情况下调用此方法。apply()函数返回存储在quiz中的对象。
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

虽然使用作用域函数不是获得预期输出的必要条件,但以上示例说明了它们如何使您的代码更简洁并避免重复相同的变量名。

以上代码仅演示了两个示例,但鼓励您在稍后课程中遇到其用法时,将作用域函数文档添加书签并参考。

9. 总结

您刚刚有机会看到几个新的 Kotlin 功能在起作用。泛型允许将数据类型作为参数传递给类,枚举类定义一组有限的可能值,数据类有助于自动为类生成一些有用的方法。

您还了解了如何创建一个单例对象(仅限于一个实例),如何使其成为另一个类的伴生对象,以及如何使用新的只读属性和新方法扩展现有类。最后,您看到了作用域函数如何在访问属性和方法时提供更简单的语法的一些示例。

在学习更多关于 Kotlin、Android 开发和 Compose 的知识时,您将在后面的单元中看到这些概念。现在,您对它们的工作原理以及它们如何提高代码的可重用性和可读性有了更好的了解。

10. 了解更多