KT-3 类与构造函数
批量生产对象
还记得我们先前介绍的对象(Object) 吗?所谓对象,就是把一些变量(称作属性)和函数(称作方法)打包在一起,并且它们可以互相访问彼此:
object Pickaxe {
var name: String = "钻石镐"
var durability: Int = 1000
var material: Material = ...
fun canBreak(block: Block): Boolean {
// 这里可以使用 name, durability, material 以及 repairWithXP
// 函数始终可以调用自身,因此不单独列出
}
fun repairWithXP(xp: Int): Unit {
// 这里同样可以使用 name, durability, material 以及 canBreak
}
}
这可以创建关于一把镐的对象,但是在实际应用中,对象的数目可能非常多。在 Minecraft 中下矿的时候,为了节约铁资源,常常用石镐替代铁镐,而石镐的磨损又很快,因此许多 Minecraft 矿工身上常带着大量的石镐。每把石镐都有自己的名称、材质和耐久度,因此需要为它们分别创建对象。如果一位玩家有十个装满石镐的大箱子(这并不算多)呢?如果服务器上有几十个玩家呢?这就得编写很多很多的 object! 即使是复制粘贴在这么多对象面前也显得非常无力。
你或许会想,要是有一些函数能够复制对象就好了,但其实不用这么麻烦,Kotlin 已经为我们提供了批量生产对象的方法,那就是类(Class)。
类是对象的蓝图,它能用来描述一系列相似的对象。什么叫做相似呢?要满足以下的条件:
- 两个对象须具有相同的属性,类型也需要相同(属性值不必相同)。
- 两个对象的行为(方法)必须相同。
例如在 Minecraft 中就有一个 Player 类用来描述所有的玩家对象。每个玩家都拥有名称,尽管名称的内容可能不同,但它肯定存在!同样,每个玩家对象也拥有相同的方法:heal 恢复生命值,ban 从服务器上封禁,等等。这些对于每个 Player 都是一样的。
定义一个类和定义一个对象很相似,只是要使用 class 关键字:
class Pickaxe {
var name: String = "钻石镐"
var durability: Int = 1000
fun repairWithXP(xp: Int): Unit {
// ...
}
}
我们稍微裁剪了一点代码来让示例代码短小一些。
这里要注意,给 name 和 durability 赋的值并不是给 Pickaxe 类的对应属性赋值 —— 因为属性是属于对象的,不是属于类的,就像蓝图描绘了一座房子的设计方案,但你不可能住在一张蓝图里!这些值是在创建对象时使用的默认值,也就是说,如果在创建对象时没给这些属性提供值,就会使用在类当中指定的默认值。
基于同样的原因,不能直接通过 Pickaxe.xxx 访问对应的方法或属性,因为方法和属性只有在作用在对象上时才有意义。先前使用 object Pickaxe 时,它是一个具体对象,而现在使用 class Pickaxe,它是一张蓝图。你要修复的是一把镐,而不是它的蓝图!
当定义好一个类后,就可以使用它来『生产』很多对象,术语称作实例化(Instantiate),顾名思义,把抽象的蓝图转换成具体的物品:
val myPickaxe = Pickaxe()
和方法调用很相似,只需要使用 类名(),就可以创建指定类的一个对象,称作实例(Instance)。为什么和方法调用很相似呢?因为 Pickaxe 除了是一个类名,它确实也代表着一个方法,我们将在稍后介绍它具体代表哪个方法。
你是职业选手吗?
与 Java 不同,在 Kotlin 中,新建对象不需要使用 new 关键字。(Python 开发者应该很理解这一点)
在创建了对象后,你就能和先前一样使用 . 来访问属性和方法了:
myPickaxe.name = "高速共振排障装置"
myPickaxe.repairWithXP(9999)
不管是对哪个对象读写属性或调用方法,都只会影响那个对象本身,而不会影响其它使用同一个类创建的对象。
这很符合直觉,不然你箱子里的数十把石镐就会在你手上的镐坏掉时一并消失了!
出厂设置
在使用类创建了新对象后,我们通常都会给属性赋值,称作初始化(Initialization)。例如,当创建新玩家时,需要给玩家对象的 name 填入正确的玩家名。这些信息不可能在定义类时就知道,因此也就无法作为默认值,必须在玩家连接到服务器后,创建玩家对象时才能填写。
当这样的属性变多的时候,要初始化一个对象就会特别麻烦:
val player = Player()
player.name = "HIM"
player.removed = "Removing"
player.ip = "0.0.0.0"
player.clientVersion = "1.21.4"
player.locale = "en-US"
player. // 受不了了,我再也不写代码了!
一次两次也就算了,可是要用到玩家的地方很多,一个个地这样初始化,肯定不是办法。有没有什么方法来抽取这些共通的『初始化』动作呢?你应该已经想到,函数可以用来打包和反复利用一系列操作,所以你可以定义一个函数来创建和初始化对象:
fun createPlayer(): Player {
val p = Player()
p.name = "HIM"
// ...
return p
}
但实际上不需要这么麻烦,Kotlin 的类已经内置了这样的功能。假设我们要在创建镐的时候指定名字,我们可以这么做:
class Pickaxe(aName: String) {
val name: String
val durability: Int = 1000
init {
name = aName
}
fun repairWithXP(xp: Int): Unit {
// ...
}
}
每一个 Kotlin 类都有一个或几个构造函数(Constructor,CTOR),所谓『构造』函数,就是专门用来创建对象的函数。如果在创建类的时候没有手动编写构造函数,Kotlin 会自动生成一个。当我们在使用 val player = Player() 这样的语法时,实际上就是在调用构造函数,所以它的语法和函数调用相同 —— 因为它就是函数。
默认情况下,构造函数只是简单地根据类定义创建一个相应的对象,然后将它返回回来。我们可以像上面那样,通过添加一个 init {} 块,来追加构造函数的行为,即让构造函数在创建对象后,还执行我们定义的操作。init 的位置很重要,一般放在所有属性之后,所有方法之前。
为什么说是『追加』呢?因为构造函数会做一些涉及 Kotlin 语言底层的操作(加载类、分配内存、链接继承关系等),这些操作在创建对象时是必要的,某种程度上也可以说对我们的代码是『保密』的,Kotlin 不允许我们修改它们。我们只能在对象创建好了之后对对象做一些额外的操作,但不能干涉对象的创建过程。
默认的构造函数不接受任何参数,如果我们想为构造函数添加额外的参数,需要在类名之后增加 (),并将参数填在其中。在上面的例子中,我们的构造函数接受一个名为 aName 的字符串,并将它赋给 name,完成初始化。
构造函数参数和任何一个属性名称都不能相同,否则 Kotlin 就分不清你指的到底是属性还是构造函数参数了。
如果一个类的构造函数包含参数,就需要在构造对象时提供:
val pk = Pickaxe("My Pickaxe") // "My Pickaxe" 被代入构造函数的 `aName` 参数,随后在 `init` 块中赋给 `name`
println(pk.name) // My Pickaxe
不提供这些参数会导致 Kotlin 抛出编译错误。
Nyaci:用
init来给属性赋值还是太麻烦了,有没有什么更简单的方式呢?
如果我说没有的话,你可以保证不去查 Kotlin 的文档吗?好的,那么这就是最简单的方式了,要写这么多啰啰嗦嗦的代码确实很麻烦,但就是这样,没什么办法……哇,别生气别生气,我是开玩笑的。作为一门以便捷性著称的语言,这么不人性化的操作,肯定有简化的方法。
简化构造函数
首先要说明的是,由于属性也是变量,我们可以在定义属性的同时使用构造函数的参数为它们赋值:
class Pickaxe(aName: String) {
val name: String = aName // aName 可以在这里使用
val durability: Int = 1000
// 这部分就不再需要了
// init {
// name = aName
// }
fun repairWithXP(xp: Int): Unit {
// ...
}
}
然后,Kotlin 设计师发现这种『把构造函数参数传递给属性』的用法实在是太常见了,所以我们可以把属性的定义挪到构造函数参数的相应位置上,Kotlin 将会自动把那个位置的参数代入相应的属性:
class Pickaxe(val name: String) { // 用属性定义替换掉参数的位置
// 这行甚至也不需要了
// val name: String = aName
val durability: Int = 1000
// 这部分就不再需要了
// init {
// name = aName
// }
fun repairWithXP(xp: Int): Unit {
// ...
}
}
当这么写了之后,我们还是可以用相同的写法创建对象:
val pk = Pickaxe("My Pickaxe")
"My Pickaxe" 被作为 Pickaxe 构造函数的第一个参数传入,Kotlin 发现那里是一个属性,于是就自动把 "My Pickaxe" 赋值给 name,就这么简单!
就这样,你已经了解了 Kotlin 当作最为核心的概念之一 —— 类。由于类的概念是如此的复杂而又抽象,我们来整理一下:
- 类是用来描绘对象的蓝图,可以用类来批量生产对象。
- 属于同一个类的对象拥有相同的行为(方法)和属性条目,尽管属性的具体值可能不同。
- 各个对象独立地拥有各自的属性值。
- 要创建对象,需要使用构造函数。Kotlin 为每个类生成默认的构造函数。
- 要在创建对象后做额外的工作,可以用
init将其添加到类定义中。 - 构造函数可以接受参数,写在类名后的
()中。 - 对属性的赋值可以简化,只需要把属性放在构造函数参数的相应位置。
不过上述的这么多内容依然不是类与对象的全部。在下一小节我们会介绍继承 —— 这个令面向对象编程真正广泛应用的核心科技。