早在第 1 章,我们就学习了 Kotlin 中的各种类型,如 Double、String 和 Boolean。在本章中,我们将开始创建我们自己的类型。
将变量和函数组合在一起
你可能还记得,我们在这本书的开头创建了变量来保存与圆相关的数据,比如半径和周长。在第 2 章中,我们创建了一个函数来根据半径计算周长。
一切都看起来有点混乱,像这样:
对于这样简单的例子来说还算可以管理,但当我们想要了解关于圆的更多信息时,比如直径、面积或位置,管理所有这些不同的变量和函数就会变得相当困难。而一旦我们开始引入其他形状,比如矩形和三角形,事情就变得更加复杂了——例如,哪些函数计算圆的面积,哪些又计算矩形的面积?
所以在本章中,我们将创建一个名为 Circle 的新类型。这样,我们就不用分别用变量和函数来保存半径和周长,而是可以有一个单一的变量来代表圆本身。
所以与上面那些混乱的变量和函数相比,它看起来会更像这样:
在 Kotlin 中,我们可以使用类(class)将相关的变量和函数组合在一起,这是一个用来创建新的 Circle 类型的特性。
定义类
让我们创建我们的第一个类——Circle,它将包含以下变量和函数:
radius变量- 只读
pi变量 circumference()函数
我们不用一次性全部完成,而是慢慢地构建这个类……一步一步来……仔细看看每个部分。
空类
首先,让我们定义一个空类——没有变量也没有函数。
class Circle创建一个名为 Circle 的新类就是这么简单!
当我们创建一个类时,我们正在创建一种新类型,就像 Int、Double 和 String 一样。换句话说,一旦我们像这样定义了 Circle 类,就可以在任何通常放置类型的地方使用它,比如作为函数参数。
fun draw(circle: Circle) {
// Code that draws the circle would go here
}我们的第一个图表
对你的类进行图表化通常很有用,这样你就可以可视化它们并与朋友交流你的想法。在本书的其余部分,我们也将使用图表来帮助解释概念。
因此,在构建这个 Circle 类时,我们也会在每个阶段画出它的图表,使用一种叫做统一建模语言(Unified Modeling Language)的图表标准,简称UML。
我们从简单的开始。要展示一个类,只需把类的名字放在一个方框里,就像这样。(我还会在图表旁边添加 Kotlin 代码,这样你就可以对比它们。)
|
|
|
现在我们有了新的 Circle 类型,可以创建一个变量来保存它,但由于我们的类完全是空的,它目前还没有什么实际用处。所以在此之前,让我们给我们的 Circle 一个 radius!
添加 radius 属性
在现实中,每个圆都有半径,所以我们需要确保代码中的每个 Circle 也有一个 radius 变量。下面是我们如何做到这一点:
class Circle(var radius: Double)在这里,我们向类中添加了一个名为 radius 的新变量,它的类型是 Double。radius 的值可以改变,因为前面有 var。在 Kotlin 中,类中的这样的变量被称为类的属性(property)。
让我们也更新图表以显示 Circle 类有一个 radius 属性。要做到这一点,我们在类名的下方画一条水平线,并写出属性的名称和类型,与我们在 Kotlin 代码中的写法几乎完全相同:
|
|
|
现在我们有了带半径的圆类,准备好开始使用它了!
对象
在本书中,我们已经创建了很多变量。例如,下面是我们如何声明和赋值一个 Double 类型的变量:
val radiusOfSmallCircle: Double = 5.2如前所述,当我们创建这个类时,我们制作了一个名为 Circle 的新类型。就像你可以有一个 Double 类型的变量一样,你也可以有一个 Circle 类型的变量。声明和赋值一个 Circle 变量很容易:
val smallCircle = Circle(5.2)这创建了一个名为 smallCircle 的新变量,它被赋值为一个半径为 5.2 的 Circle。
请记住——就像你可以有许多不同的 Double 值一样……
5.26.710.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 类型的参数:
请注意,radius 实际上扮演着两个角色:
- 它是构造函数参数。每次调用构造函数时都必须为其提供一个值。(但是,与函数参数一样,你也可以为构造函数参数提供默认参数)。
- 它是属性。你能够从任何圆对象获取
radius的值。我们稍后会看到一个这样的例子。
像 radius 这样以 val 或 var 开头的构造函数参数有时也称为属性参数(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)私有属性
目前的状态下,你可以获取任何圆的 radius 和 pi 的值:
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让我们更新 UML 类图以包含 pi 属性。我们可以在图中表示属性的可见性:
- 私有属性前加
-符号 - 公共属性前加
+符号
|
|
|
现在,我们准备向类中添加 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 参数,这样它引用的就是属性,就像这样……
这引入了作用域(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() 添加到图表中!
|
|
|
请注意,图表中不包含函数的主体。换句话说,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 图中。
|
|
|
类的结构
既然我们已经涵盖了 Kotlin 类的基础知识,下面是主要内容的回顾:
一切皆为对象
在本章之前,我们只使用过 Kotlin 的内置类型,如 Double、String 和 Boolean。你可能会惊讶地发现,当我们使用这些类型时,实际上是在使用类和对象!就像我们使用点 . 来获取属性和在我们的 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 变量——它们只能是 true 或 false——也是对象!例如,如果你想在天黑或者下雨的时候打开车头灯,你可以像这样写代码:
val isDark: Boolean = true
val isRaining: Boolean = false
val shouldTurnOnHeadlights = isDark.or(isRaining)
val shouldStayHome = isDark.and(isRaining)如你所见,对象在 Kotlin 中无处不在!即使简单的值也是对象。
总结
类非常强大,尽管本章涵盖了很多内容,但我们实际上只是介绍了它们。它们开启了一种全新的方式来在代码中表示你的概念。在本书的后面部分,我们将介绍更多高级概念,如抽象和继承。但现在,你应该为自己的进步感到骄傲!
在本章中,我们终于使用类创建了我们自己的类型。我们学习了:
- 如何定义一个类
- 如何创建一个对象——即类的实例
- 如何向类添加属性,以及如何访问它们
- 如何向类添加函数,以及如何调用它们
- 如何为单个类创建 UML 图
- 如何从我们已学过的内置 Kotlin 类型(如
Double、String和Boolean)调用函数和获取属性
还有……以防你没有看够,你还可以看到Kotlin 类的广泛示例。
在下一章>中,我们将介绍一种特殊的类,叫做枚举类(enum class)。到时候见!
感谢 Tobenna Ezike 和 Esraa Ibrahim 审阅本章。