Java中,一个类可以声明一个或多个构造函数。Kotlin也是相似的,但有一个额外的改变:它在主构造函(primary constructor),通常是主要的,简洁的方式来初始化一个类并且被声明在类的主体外部的) 和次要构造函数(secondary constructor),声明在类主体的内部)之间制造些差异。它也允许你再初始化器代码块中(initializer blocks) 放一些额外的初始化逻辑。首先,我们来看看声明主构造函数和初始化器的语法。然后我们将会解释如何声明多个构造函数。之后,我们将会更多的讨论属性。
4.2.1 初始化类:主构造器和初始化器
在第二章,你看到了如何声明一个简单的类:
class User(val nickname: String)
通常,类中所有的声明都会在闭合大括号的内部。你可能很好奇为什么这个类没有闭合的大括号而是仅仅在小括号内有一个声明。圆括号内部的代码叫做主构造函数。它有两个目的:指定构造函数参数,同时定义由这些参数初始化的属性。让我们揭开这里会发生什么并且看看你可以编写的同时实现同样功能的最直接的代码:
class User constructor(_nickname: String) { // 带有一个参数的主构造器
val nickname: String
init { // 初始化块
nickname = _nickname
}
}
在这个例子中,你看到了两个新的Kotlin关键字: constructor 和 init 。 constructor 关键字开启了一个主构造器或次构造器的声明。 init 关键字引入了一个 initializer 块。这样的块包含了当这个类通过主构造函数创建时执行的初始化代码。由于主构造器有一个限制语法,它不能包含初始化代码。这就是为什么你需要初始化块的原因。如果你想要,你可以在一个类中声明多个初始化块。 构造函数参数 _nickname 中的下划线是为了区分构造函数参数名的属性名。一个可选的方案是使用同样的名字的同时写上 this 来避免歧义,就像Java中经常做的那样: this.nickname = nickname 。 在这个例子中,你不需要在初始化块中放置初始化代码。因为它可以跟 nickname 属性声明合并。你也可以省略 constructor 关键字,如主构造函数没有标记或者可见性修饰符。如果你应用了这些改变,你可以得到下面的代码:
class User(_nickname: String) { // 带有一个参数的主构造函数
val nickname = _nickname // 属性被参数初始化
}
这有另一种方法来声明同样的类。注意,你是如何能够引用属性初始化器和初始化代码中的主构造函数参数的。 前面的两个例子在类主体中使用 val 关键字声明的属性。如果属性被初始化为对应的构造函数参数,这份代码可以简化为在参数面前添加 val 关键字。这将替代类中的属性定义:
class User(val nickname: String) // 1 "val"意味着对应的属性是为构造函数参数生成的
User 类的以上所有声明都是等价的,但是最后一种使用了最精简的语法。 你可以为构造函数参数声明默认值,就像函数参数那样:
class User(val nickname: String,
/**
* 为构造函数参数提供默认值
*/
val isSubscribed: Boolean = true)
为了创建一个类的实例,你可以直接调用构造函数,而无需 new 关键字:
>>> val alice = User("Alice") // 1 为isSubscribed参数使用默认值"true"
>>> println(alice.isSubscribed)
true
>>> val bob = User("Bob", isSubscribed = false) // 2 你可以为某些构造函数参数显式的指定名字
>>> println(bob.isSubscribed)
false
如果所有的构造函数参数都有默认值,编译器会额外生成一个使用了所有默认值但没有参数的构造函数。这使得通过无参数构造函数初始化类的库来使用Kotlin变得更加容易。
如果你的类有一个超类,主构造函数也需要初始化超类。你可以通过在基类列表中的超类引用后面提供超类构造函数来达到这个目的:
open class User(val nickname: String) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }
如果你没有为某个类声明任意的构造函数,编译器会为你生成一个什么也不干的默认构造函数:
open class Button // 生成一个没有参数的默认构造函数
这就是为什么你需要在超类名的后面有一个空括号。注意跟接口的不同:接口没有构造函数,所以如果你实现了一个接口,你绝对不能在它的超类列表名字后面放置圆括号。 如果你想确保你的类不能被其他代码初始化,你必须让构造函数私有。以下是你如何让主构造函数私有的:
class Secretive private constructor() {} // 这个类有一个私有构造函数
作为替代方案,你能够以在类主体内,一个更加常见的方式来声明它:
class Secretive {
private constructor()
}
由于 Secretive 类只有一个私有构造函数,类外部的代码不能初始化它。后续部分,将会讨论伴生对象。它可能是放置调用这样的构造函数的好地方。
4.2.2 次构造函数:以不同的方式初始化超类
一般来讲,有多个构造函数的类在Kotlin代码中没有Java那么常见。在Java,你需要重载构造函数主要的情景被Kotlin对默认参数值的支持覆盖了。
但是任然有需要多个构造函数的情景。最常见的一个情形出现了,当你需要扩展一个提供多个以不同方式初始化类的构造函数的框架类。想象一个,声明在Java中,有两个构造函数(如果你是一个Android开发者,你可能认得这个定义) 的 View 类。Kotlin中的一个相似的声明将会跟下面的代码一样:
open class View {
constructor(ctx: Context) { // 次构造函数
// some code
}
constructor(ctx: Context, attr: AttributeSet) { // 次构造函数
// some code
}
}
这个类并没有声明一个主构造函数(跟你想说的一样,因为在类头部的名字后面没有圆括号) ,但是它声明了两个次构造函数。通过使用 constructor 关键字引入了一个次构造函数。
你可以声明跟你所需的一样多的次构造函数。 如果你想要扩展这个类,你可以声明同样的构造函数:
class MyButton : View {
constructor(ctx: Context)
: super(ctx) { // 调用超类构造函数
// ...
}
constructor(ctx: Context, attr: AttributeSet)
: super(ctx, attr) { // 调用超类构造函数
// ...
}
}
你在这里定义了两个构造函数,每一个都通过使用 super() 关键字调用了对应的超类构造函数。下图解释了这一点。一个箭头展示委托了那个构造函数。
image.png
就像Java那样,你也有一个可选的权利使用 this() 关键字从一个构造函数调用你的类中的其他构造函数。以下是它如何工作的:
class MyButton : View {
constructor(ctx: Context) : this(ctx, MY_STYLE) { // 委托给类中的其他构造函数
// ...
}
constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {
// ...
}
}
你可以改变 MyButton 类,来将其中一个构造函数委托给类中的其他构造函数,并为参数传递了默认值。如下图所示,次构造函数继续调用 super() 。
image.png
如果类没有主构造函数,那么每一个次构造函数必须初始化基类或者委托其他做这些事的构造函数。
4.2.3 实现声明在接口中的属性
在Kotlin中,一个接口可以包含抽象属性声明。这有一个声明这样一个接口定义的例子:
interface User {
val nickname: String
}
这意味着实现了 User 接口的类需要提供一种方式来和获取 nickname 的值。接口并没有指定一个是否应该存储在一个备份字段或者通过一个 getter 来获取。因此,接口本身不包含任何状态。如果有需要的话,也只有实现了接口的类能够存储值。 让我们来看看接口的一些可能的实现: PrivateUser ,只填充他们的昵称; SubscribingUser ,被迫提供一个邮箱来注册; FacebookUser ,简单的共享了他们的Facebook账号ID。所有的这些类都以不同的方式实现了接口中的抽象属性:
class PrivateUser(override val nickname: String) : User // 主构造函数属性
class SubscribingUser(val email: String) : User {
override val nickname: String
get() = email.substringBefore('@') // 自定义getter属性初始化器
}
class FacebookUser(
val accountId: Int) : User {
override val nickname = getFacebookName(accountId) // 属性初始化器
}
>>> println(PrivateUser("282921012@qq.com").nickname)
282921012@qq.com
>>> println(SubscribingUser("282921012@qq.com").nickname)
282921012
对于 PrivateUser ,你使用精简的语法来直接声明一个主构造函数中的属性。这个属性实现了来自 User 类的抽象属性,因此你把它标记为 override 。 对于 SubscribingUser , nickname 属性通过一个自定义的getter来实现。这个属性并没有支持字段(backing field) 来存储它的值。它只有一个计算来自每个调用中的邮箱的昵称的getter。
对于 FacebookUser ,你可以在它的初始化器中为 nickname 属性分配值。你使用一个支持返回给定IDFacebook用户名称的 getFacebookName 函数(假设它在某个地方被定义了) 。这个函数是代价昂贵的:他需要跟Facebook建立一个连接来得期望的数据。这就是为什么你决定在初始化阶段调用它一次。 注意 nickname 在 SubscribingUser 和 FacebookUser 中的不同实现。尽管它们看上去很相似,第一个属性有一个在每次访问时计算 substringBefore 的自定义的getter,然而 FacebookUser 中的属性有一个存储类初始化时计算的结果的支持字段。 除了抽象属性声明之外,接口也可以包含带有多个getter和setter属性,只要它们不引用支持字段(支持字段要求在一个接口中存储状态,而这是不允许的) 。 然我们来看一个例子:
interface User {
val email: String
val nickname: String
get() = email.substringBefore('@') // 属性并没有支持字段:每次调用都要重新计算结果
}
这个字段包含了抽象属性 email ,同时 nickname 属性带有自定义的getter。第一个属性在子类必须被覆盖,然而第二个可以被继承。 跟接口中实现的属性不同,类中实现的属性拥有支持字段的全部访问权限。让我们看看你如何能够通过访问器引用它们。
4.2.4 通过getter或者setter访问支持字段
你已经看到了有关两种属性的一些例子:保存值的属性和带有每次访问都就进行计算的自定义访问器的属性。现在让我们来看看你如何能够合并两者同时实现有一个保存值的属性并提供当值被访问或者修时才执行的额外逻辑。为了支持这一点,你需要能够从它的访问器访问属性的支持字段。 举个例子,让我们假设你想要记录属性中存储的数据的变化。你声明了一个可变属性并在setter的每一次访问时执行了额外的代码:
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value".""".trimIndent()) // 读取支持字段的值 更新支持字段的值
field = value // 更新支持字段的值
}
}
>>> val user = User("Alice")
>>> user.address = "Elsenheimerstraße 47, 80687 München"
Address was changed for Alice:
"unspecified" -> "Elsenheimerstraße 47, 80687 München"
像往常那样,通过声明 user.address = "new value" ,这实际上调用了一个setter,你改变了属性的值。在这个例子中,setter被重新定义了。所以额外的日志代码被执行了(为了简单起见,在这个例子中你仅仅把它打印出来) 。 在setter内部,你使用特殊的标记符 field 来访问支持字段的值。在getter中,只能读取值。在setter中,你既可以读取又可以修改它。 注意,你只能为可变属性重定义其中一个访问器。前一个例子中的getter是不重要的,它仅仅返回了字段的值。所以你不需要重新定义它。 你可能会好奇是什么导致了有支持字段和没有支持字段的属性之间的差异呢?你访问它的方式不依赖于属性是否有支持字段。编译器将会为属性产生支持字段如果你显式的引用它或者使用了默认访问器的实现。如果你提供了一个不使用 field 的自定义访问器实现,支持字段不会出现。 有时候,你不需要改变访问器的实现,按时你需要改变它的可见性。让我们来看看你如何能做到这一点。
4.2.5 改变访问器的可见性
访问器的可见性默认是跟属性的一样的。但是如果你想的话,通过在 get 或者 set 关键字之间放置可见性修饰符,你可以改变这一点。为了看看你可以如何使用它,让我们来看一个例子:
class LengthCounter {
var counter: Int = 0
private set // 你无法在类的外部改变这个属性
fun addWord(word: String) {
counter += word.length
}
}
这个类计算加入到它当中的单词的总长度。由于它是类向客户端提供的API的一部分,保存总长度的属性是公开访问的。但是,你需要确保它只能在类中被修改。因为不这样做的话,外部代码可以改变它并存入一个不正确的值。所以,你让编译器生成一个带有默认可见性的getter。然后你将setter的可见性改为 private 。 下面的代码演示了你可以如何使用这个类:
>>> val lengthCounter = LengthCounter()
>>> lengthCounter.addWord("Hi!")
>>> print(lengthCounter.counter)
3
你创建了一个 LengthCounter 实例。然后你添加了一个单词"Hi!"的长度3。现在 counter 的属性保存的是3。
以上内容总结了我们有关如何在Kotlin编写重要构造器和属性的讨论。接下来,你将会看到如何使用 data 类的概念来让值-对象类变得更加友好。
网友评论