1. 上一章:第 3 章
  2. 下一章:第 5 章
Kotlin:图解指南 • 第 4 章

类和对象简介

章节封面图片

早在第 1 章,我们就学习了 Kotlin 中的各种类型,如 DoubleStringBoolean。在本章中,我们将开始创建我们自己的类型。

将变量和函数组合在一起

你可能还记得,我们在这本书的开头创建了变量来保存与圆相关的数据,比如半径和周长。在第 2 章中,我们创建了一个函数来根据半径计算周长。

一切都看起来有点混乱,像这样:

针对不同大小的圆的许多 radius 和 circumference 变量

对于这样简单的例子来说还算可以管理,但当我们想要了解关于圆的更多信息时,比如直径、面积或位置,管理所有这些不同的变量和函数就会变得相当困难。而一旦我们开始引入其他形状,比如矩形和三角形,事情就变得更加复杂了——例如,哪些函数计算圆的面积,哪些又计算矩形的面积?

所以在本章中,我们将创建一个名为 Circle 的新类型。这样,我们就不用分别用变量和函数来保存半径和周长,而是可以有一个单一的变量来代表圆本身

所以与上面那些混乱的变量和函数相比,它看起来会更像这样:

相同的变量,但所有相关变量都根据其所代表的圆进行了分组

在 Kotlin 中,我们可以使用类(class)将相关的变量和函数组合在一起,这是一个用来创建新的 Circle 类型的特性。

定义类

让我们创建我们的第一个类——Circle,它将包含以下变量和函数:

  • radius 变量
  • 只读 pi 变量
  • circumference() 函数

我们不用一次性全部完成,而是慢慢地构建这个类……一步一步来……仔细看看每个部分。

空类

首先,让我们定义一个空类——没有变量也没有函数。

class Circle

创建一个名为 Circle 的新类就是这么简单!

当我们创建一个类时,我们正在创建一种新类型,就像 IntDoubleString 一样。换句话说,一旦我们像这样定义了 Circle 类,就可以在任何通常放置类型的地方使用它,比如作为函数参数

fun draw(circle: Circle) {
  // Code that draws the circle would go here
}

我们的第一个图表

对你的类进行图表化通常很有用,这样你就可以可视化它们并与朋友交流你的想法。在本书的其余部分,我们也将使用图表来帮助解释概念。

因此,在构建这个 Circle 类时,我们也会在每个阶段画出它的图表,使用一种叫做统一建模语言(Unified Modeling Language)的图表标准,简称UML

我们从简单的开始。要展示一个类,只需把类的名字放在一个方框里,就像这样。(我还会在图表旁边添加 Kotlin 代码,这样你就可以对比它们。)

一个非常简单的 UML 类图,用于表示一个名为 Circle 的空类
class Circle

现在我们有了新的 Circle 类型,可以创建一个变量来保存它,但由于我们的类完全是空的,它目前还没有什么实际用处。所以在此之前,让我们给我们的 Circle 一个 radius

添加 radius 属性

在现实中,每个圆都有半径,所以我们需要确保代码中的每个 Circle 也有一个 radius 变量。下面是我们如何做到这一点:

class Circle(var radius: Double)

在这里,我们向类中添加了一个名为 radius 的新变量,它的类型是 Doubleradius 的值可以改变,因为前面有 var。在 Kotlin 中,类中的这样的变量被称为类的属性(property)

让我们也更新图表以显示 Circle 类有一个 radius 属性。要做到这一点,我们在类名的下方画一条水平线,并写出属性的名称和类型,与我们在 Kotlin 代码中的写法几乎完全相同:

UML 类图,用于表示一个具有名为 radius 的单一属性的 Circle
class Circle(var radius: Double)

现在我们有了带半径的圆类,准备好开始使用它了!

对象

在本书中,我们已经创建了很多变量。例如,下面是我们如何声明赋值一个 Double 类型的变量:

val radiusOfSmallCircle: Double = 5.2

如前所述,当我们创建这个类时,我们制作了一个名为 Circle 的新类型。就像你可以有一个 Double 类型的变量一样,你也可以有一个 Circle 类型的变量。声明和赋值一个 Circle 变量很容易:

val smallCircle = Circle(5.2)

这创建了一个名为 smallCircle 的新变量,它被赋值为一个半径为 5.2Circle

请记住——就像你可以有许多不同的 Double 值一样……

  • 5.2
  • 6.7
  • 10.0

……你也可以有许多不同的 Circle 值,比如……

  • 半径为 5.2 的圆
  • 半径为 6.7 的圆
  • 半径为 10.0 的圆

但当涉及到类时,我们通常不把这些叫做,而是称它们为对象(objects)

构造对象

让我们再看看那段代码。

// Declaring the class
class Circle(var radius: Double)

// Using the class
val smallCircle = Circle(5.2)

当我们写 Circle(5.2) 时会发生什么?

这有点像我们正在调用一个名为 Circle() 的函数,它有一个名为 radius 的参数和 Circle返回类型。但这类函数不叫函数,而是被称为构造函数(constructors),因为它们构造一个新对象。

构造函数参数

请注意,当你调用构造函数时,必须为构造函数左括号和右括号 () 之间列出的每个属性提供参数。由于我们在括号之间放了 var radius: Double,所以在调用构造函数时必须提供一个 Double 类型的参数:

5.2 参数作为 radius 参数传递给构造函数

请注意,radius 实际上扮演着两个角色:

  1. 它是构造函数参数。每次调用构造函数时都必须为其提供一个值。(但是,与函数参数一样,你也可以为构造函数参数提供默认参数)。
  2. 它是属性。你能够从任何圆对象获取 radius 的值。我们稍后会看到一个这样的例子。

radius 这样以 valvar 开头的构造函数参数有时也称为属性参数(property parameter),因为它既是属性又是构造函数参数。

当你创建一个对象时,我们称你正在创建该类的实例(instance)。因此,创建对象有时也被称为实例化(instantiating)该类。

类与对象

类和对象之间的区别一开始可能会让人困惑,所以让我们花点时间来澄清一下。

描述了某个概念的特征和行为。如果我们讨论圆,这些特征可能包括半径、直径、周长和面积。

对象是那个事物的一个实际的特定实例。这里有三个圆对象:

不同大小的圆

圆的回答这样的问题:

  • ”什么东西可以被称为圆?”
  • ”它有什么特征?”
  • ”它能做什么?”

圆的对象回答这样的问题:

  • 这个特定的圆的半径是多少?”
  • ”它的周长是多少?”
  • ”它的面积是多少?”

这里还有更多例子来帮助区分类和对象。

  • 你可能有一个数字(Number)类,其对象如 32,768 或 6.62607015。
  • 你可能有一个颜色(Color)类,其对象如红色、绿色和蓝色。
  • 你可能有一个狗(Dog)类,其对象如 Fido、Rover 或 Mrs. Wagglytails。
多只狗,代表同一类的多个对象

在本书的其余部分,我们将看到更多关于类和对象的例子!

获取属性的值

现在我们已经创建了一个圆对象,如何获取它的半径?

很简单:要获取对象的属性值,只需输入变量的名称、一个 .,然后是属性的名称,像这样:

val smallCircle = Circle(5.2)
val radiusOfSmallCircle: Double = smallCircle.radius

运行该代码后,radiusOfSmallCircle 将等于 5.2

半径已经搞定了。现在是时候来看看另一个属性 pi 了。

常量属性

我们可以像对 radius 那样把 pi 放在括号里,用逗号分隔:

class Circle(var radius: Double, val pi: Double)

但如果我们这样做,那么每次实例化一个圆时,我们都必须提供 pi,像这样:

val smallCircle = Circle(5.2, 3.14)
val mediumCircle = Circle(6.7, 3.14)
val largeCircle = Circle(10.0, 3.14)

嗯……这不太符合我们的需求。因为 pi 应该始终是相同的值,不管特定的圆如何,它作为构造函数参数是没有意义的。

实际上,最好是让调用代码在构造 Circle永远无法指定 pi 的值。

要做到这一点,我们可以简单地把 pi 移到括号外面,就像这样:

class Circle(var radius: Double) {
  val pi: Double = 3.14
}

在这里,我们添加了一个左花括号 { 和一个右花括号 }。这两个花括号之间的所有内容被称为类的主体(body)。在主体内部,我们声明了 pi,并给它赋值 3.14

通过将它移出括号,pi 属性不再是构造函数参数,因此我们可以继续像在清单 4.5中那样仅用半径调用构造函数:

val smallCircle = Circle(5.2)

私有属性

目前的状态下,你可以获取任何圆的 radiuspi 的值:

val smallCircle = Circle(5.2)

val radiusOfSmallCircle = smallCircle.radius
val piFromSmallCircle = smallCircle.pi

我想不出太多类外部代码需要 pi 值的理由。让我们把这个属性设置为只能从类的内部看到。为此,我们可以在声明时添加 private 关键字。

class Circle(var radius: Double) {
  private val pi: Double = 3.14
}

现在,如果你试图从类的外部获取 pi 属性的值,就会收到一个错误:

val smallCircle = Circle(5.2)

val radiusOfSmallCircle = smallCircle.radius
val piFromSmallCircle = smallCircle.pi
Error

让我们更新 UML 类图以包含 pi 属性。我们可以在图中表示属性的可见性

  • 私有属性前加 - 符号
  • 公共属性前加 + 符号
UML 类图,展示了 Circle 的两个属性,包括可见性指示符
class Circle(var radius: Double) {
    private val pi: Double = 3.14
}

现在,我们准备向类中添加 circumference() 函数!

添加成员函数

当一个函数属于一个类时,它通常被称为方法(method)成员函数(member function)。向类中添加方法很容易——只需将它放入类的主体中。首先,让我们直接使用 清单 2.3 中的同一个函数原封不动地放到我们的 Circle 类中:

class Circle(var radius: Double) {
  private val pi: Double = 3.14

  fun circumference(radius: Double) = 2 * pi * radius
}

当我们在第 2 章首次创建 circumference() 函数时,让它有一个名为 radius 的参数是合理的。但现在我们将这个函数添加到中,就可以直接引用 radius 属性的值了。

换句话说,不是像这样引用 radius 参数……

函数体内的 radius 指向 circumference 函数的 radius 参数

……我们可以从函数中移除 radius 参数,这样它引用的就是属性,就像这样……

函数体内的 radius 指向 circumference 函数的 radius 参数

这引入了作用域(scope)的概念,我们将在后面的章节中深入探讨。现在,只需要确保从 circumference() 函数中移除参数,这样 2 * pi * radius 就会引用 radius 属性。

class Circle(var radius: Double) {
  private val pi: Double = 3.14

  fun circumference() = 2 * pi * radius
}

现在 Circle 有了 circumference() 函数,我们如何调用它呢?

在对象上调用函数的方式与获取 radius 值的方式类似:变量的名称、一个点 .,然后是函数的名称。

val smallCircle = Circle(5.2)
val circumferenceOfSmallCircle: Double = smallCircle.circumference()

在我们继续之前,让我们把 circumference() 添加到图表中!

UML 类图,包含两个属性和一个函数
class Circle(var radius: Double) {
  private val pi: Double = 3.14

  fun circumference() = 2 * pi * radius
}

请注意,图表中不包含函数的主体。换句话说,2 * pi * radius 不会出现在图表中。这是因为类图表的目的是让你了解哪些数据和行为是类的一部分,而不涉及具体实现细节。

添加更多函数

当我们开始本章时,我们只有一个 radius 变量和一个 circumference() 函数。现在我们已经把这两样东西组合成一个类,是时候用你可能想了解的关于圆的其他东西来填充我们的 Circle 类了。

例如,我们可以很容易地添加一个计算圆的面积的函数。

class Circle(var radius: Double) {
  private val pi: Double = 3.14

  fun circumference() = 2 * pi * radius
  fun area() = pi * radius * radius
}

val smallCircle = Circle(5.2)
val areaOfSmallCircle = smallCircle.area()

我们还可以添加一个计算其直径的函数。

class Circle(var radius: Double) {
  private val pi: Double = 3.14

  fun circumference() = 2 * pi * radius
  fun area() = pi * radius * radius
  fun diameter() = 2 * radius
}

val smallCircle = Circle(5.2)
val diameterOfSmallCircle = smallCircle.diameter()

我们甚至可以改变计算周长的方式,改为使用 diameter() 函数:

class Circle(var radius: Double) {
  private val pi: Double = 3.14

  fun circumference() = diameter() * pi
  fun area() = pi * radius * radius
  fun diameter() = 2 * radius
}

现在,我们可以把这些最后的函数添加到 UML 图中。

UML 类图,用于表示具有多个属性和函数的 Circle
class Circle(var radius: Double) {
  private val pi: Double = 3.14

  fun circumference() = diameter() * pi
  fun area() = pi * radius * radius
  fun diameter() = 2 * radius
}

类的结构

既然我们已经涵盖了 Kotlin 类的基础知识,下面是主要内容的回顾:

Kotlin 中类的结构

一切皆为对象

在本章之前,我们只使用过 Kotlin 的内置类型,如 DoubleStringBoolean。你可能会惊讶地发现,当我们使用这些类型时,实际上是在使用类和对象!就像我们使用点 . 来获取属性和在我们的 Circle 对象上调用函数一样,我们也可以在任何这些类型上使用点。

作为对象的 Double

例如,Double 类型的对象有 plus()times() 这样的函数。所以,与其像这样编写我们的 circumference() 函数……

fun circumference() = 2 * pi * radius

……我们可以像这样写……

fun circumference() = 2.times(pi).times(radius)

Kotlin 开发者通常在大多数情况下使用算术运算符+-*/),但如果你想用的话,这些函数也是存在的!

作为对象的 String

String 对象也有一些有趣的属性和函数。下面是几个例子:

  • length 属性告诉你字符串中有多少个字符(即字母、数字和符号)。
  • toUpperCase() 会将字符串中的所有字母强制转换为大写。
  • drop() 会从字符串的开头删除字符。

在代码中是这样的:

val greeting: String = "Welcome"

val numberOfLettersInGreeting = greeting.length // Evaluates to 7
val loudGreeting = greeting.toUpperCase()       // Evaluates to "WELCOME"
val substring = greeting.drop(3)                // Evaluates to just "come"

作为对象的 Boolean

即使是 Boolean 变量——它们只能是 truefalse——也是对象!例如,如果你想在天黑或者下雨的时候打开车头灯,你可以像这样写代码:

val isDark: Boolean = true
val isRaining: Boolean = false

val shouldTurnOnHeadlights = isDark.or(isRaining)
val shouldStayHome = isDark.and(isRaining)

如你所见,对象在 Kotlin 中无处不在!即使简单的值也是对象。

总结

类非常强大,尽管本章涵盖了很多内容,但我们实际上只是介绍了它们。它们开启了一种全新的方式来在代码中表示你的概念。在本书的后面部分,我们将介绍更多高级概念,如抽象和继承。但现在,你应该为自己的进步感到骄傲!

在本章中,我们终于使用创建了我们自己的类型。我们学习了:

  • 如何定义一个类
  • 如何创建一个对象——即类的实例
  • 如何向类添加属性,以及如何访问它们
  • 如何向类添加函数,以及如何调用它们
  • 如何为单个类创建 UML 图
  • 如何从我们已学过的内置 Kotlin 类型(如 DoubleStringBoolean)调用函数和获取属性

还有……以防你没有看够,你还可以看到Kotlin 类的广泛示例

下一章中,我们将介绍一种特殊的类,叫做枚举类(enum class)。到时候见!

感谢 Tobenna EzikeEsraa Ibrahim 审阅本章。