如你所知,Java中,一个类可以声明一个或多个构造函数。Kotlin也是相似的,但有一个额外的改变:它在主构造函数(primary constructor,通常是主要的,简洁的方式来初始化一个类并且被声明在类的主体外部的)和次要构造函数(secondary constructor,声明在类主体的内部)之间制造些差异。它也允许你再初始化器代码块中(initializer blocks)放一些额外的初始化逻辑。首先,我们来看看声明主构造函数和初始化器的语法。然后我们将会解释如何声明多个构造函数。之后,我们将会更多的讨论属性。

4.2.1 初始化类:主构造器和初始化器

在第二章,你看到了如何声明一个简单的类:


class User(val nickname: String)

通常,类中所有的声明都会在闭合大括号的内部。你可能很好奇为什么这个类没有闭合的大括号而是仅仅在小括号内有一个声明。圆括号内部的代码叫做主构造函数。它有两个目的:指定构造函数参数,同时定义由这些参数初始化的属性。让我们揭开这里会发生什么并且看看你可以编写的同时实现同样功能的最直接的代码:


class User constructor(_nickname: String) { // 1 带有一个参数的主构造器

    val nickname: String

    init { // 2 初始化块
        nickname = _nickname
    }
}

在这个例子中,你看到了两个新的Kotlin关键字:constructorinitconstructor关键字开启了一个主构造器或次构造器的声明。init关键字引入了一个initializer块。这样的块包含了当这个类通过主构造函数创建时执行的初始化代码。由于主构造器有一个限制语法,它不能包含初始化代码。这就是为什么你需要初始化块的原因。如果你想要,你可以在一个类中声明多个初始化块。  构造函数参数_nickname中的下划线是为了区分构造函数参数名的属性名。一个可选的方案是使用同样的名字的同时写上this来避免歧义,就像Java中经常做的那样:this.nickname = nickname。  在这个例子中,你不需要在初始化块中放置初始化代码。因为它可以跟nickname属性声明合并。你也可以省略constructor关键字,如果主构造函数没有标记或者可见性修饰符。如果你应用了这些改变,你可以得到下面的代码:


class User(_nickname: String) { // 1 带有一个参数的主构造函数
    val nickname = _nickname // 2 属性被参数初始化
}

这有另一种方法来声明同样的类。注意,你是如何能够引用属性初始化器和初始化代码中的主构造函数参数的。  前面的两个例子在类主体中使用val关键字声明的属性。如果属性被初始化为对应的构造函数参数,这份代码可以简化为在参数面前添加val关键字。这将替代类中的属性定义:


class User(val nickname: String) // 1 "val"意味着对应的属性是为构造函数参数生成的

User类的以上所有声明都是等价的,但是最后一种使用了最精简的语法。 你可以为构造函数参数声明默认值,就像函数参数那样:


class User(val nickname: String,
    val isSubscribed: Boolean = true) // 1 为构造函数参数提供默认值

为了创建一个类的实例,你可以直接调用构造函数,而无需new关键字:


>>> val alice = User("Alice")  // 1 为isSubscribed参数使用默认值"true"
>>> println(alice.isSubscribed)
true

>>> val bob = User("Bob", isSubscribed = false)  // 2 你可以为某些构造函数参数显式的指定名字
>>> println(bob.isSubscribed)
false

看上去是Alice默认订阅了邮件列表,然而Bob阅读了条款并小心的取消了默认选项。

NOTE  注意  如果所有的构造函数参数都有默认值,编译器会额外生成一个使用了所有默认值但没有参数的构造函数。这使得通过无参数构造函数初始化类的库来使用Kotlin变得更加容易。

如果你的类有一个超类,主构造函数也需要初始化超类。你可以通过在基类列表中的超类引用后面提供超类构造函数来达到这个目的:


open class User(val nickname: String) { ... }
class TwitterUser(nickname: String) : User(nickname) { ... }

如果你没有为某个类声明任意的构造函数,编译器会为你生成一个什么也不干的默认构造函数:


open class Button // 1 生成一个没有参数的默认构造函数

这就是为什么你需要在超类名的后面有一个空括号。注意跟接口的不同:接口没有构造函数,所以如果你实现了一个接口,你绝对不能在它的超类列表名字后面放置圆括号。  如果你想确保你的类不能被其他代码初始化,你必须让构造函数私有。以下是你如何让主构造函数私有的:


class Secretive private constructor() {} // 1 这个类有一个私有构造函数

作为替代方案,你能够以在类主体内,一个更加常见的方式来声明它:


class Secretive {
    private constructor()
}

由于Secretive类只有一个私有构造函数,类外部的代码不能初始化它。在这一章的后续部分,我们将会讨论伴生对象。它可能是放置调用这样的构造函数的好地方。

SIDEBAR  可选的私有构造函数  在Java中,你可以使用一个私有构造函数来禁止类实例化以表达更常见的想法:类是一个静态工具成员的容器或者是一个单例。Kotlin为这些意图准备了内置的语言特性。你把顶层函数(你在3.2.3节看到的)作为静态工具。为了表达单例,你使用对象什么,就像你在本章后续的4.4.1节看到的那样。  在大部分真实用例中,类的构造函数是直接的:它没有包含参数或者把参数分配给相应的属性。这就是为什么Kotlin的主构造函数会有精简的语法:对于大多数场景它都可以很好的工作。但是生活并不总是容易的,所以Kotlin允许你定义你需要的所有构造函数。让我们来看看这是如何工作的。

4.2.2 次构造函数:以不同的方式初始化超类

一般来讲,有多个构造函数的类在Kotlin代码中没有Java那么常见。在Java,你需要重载构造函数主要的情景被Kotlin对默认参数值的支持覆盖了。

TIP 提示  不要声明多个次构造函数来重载,同时为参数提供默认值。相反的,直接指定默认值。

但是任然有需要多个构造函数的情景。最常见的一个情形出现了,当你需要扩展一个提供多个以不同方式初始化类的构造函数的框架类。想象一个,声明在Java中,有两个构造函数(如果你是一个Android开发者,你可能认得这个定义)的View类。Kotlin中的一个相似的声明将会跟下面的代码一样:


open class View {

    constructor(ctx: Context) { // 1 次构造函数
        // some code
    }

    constructor(ctx: Context, attr: AttributeSet) { // 1 次构造函数
        // some code
    }
}

这个类并没有声明一个主构造函数(跟你想说的一样,因为在类头部的名字后面没有圆括号),但是它声明了两个次构造函数。通过使用constructor关键字引入了一个次构造函数。你可以声明跟你所需的一样多的次构造函数。  如果你想要扩展这个类,你可以声明同样的构造函数:


class MyButton : View {
    constructor(ctx: Context)
        : super(ctx) {  // 1 调用超类构造函数
        // ...
    }

    constructor(ctx: Context, attr: AttributeSet)
        : super(ctx, attr) { // 1 调用超类构造函数
        // ...
    }
}

你在这里定义了两个构造函数,每一个都通过使用super()关键字调用了对应的超类构造函数。图4.3解释了这一点。一个箭头展示委托了那个构造函数。

图4.3

图4.3 使用不同的超类构造函数

就像Java那样,你也有一个可选的权利使用this()关键字从一个构造函数调用你的类中的其他构造函数。以下是它如何工作的:


class MyButton : View {
    constructor(ctx: Context): this(ctx, MY_STYLE) { // 1 委托给类中的其他构造函数
        // ...
    }

    constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {
        // ...
    }
}

你可以改变MyButton类,来将其中一个构造函数委托给类中的其他构造函数,并为参数传递了默认值。如图4.4所示,次构造函数继续调用super()

图4.4

如果类没有主构造函数,那么每一个次构造函数必须初始化基类或者委托其他做这些事的构造函数。想一下之前的那张图,每一个次构造函数必须有一个对外的箭头为路径的开始,并在基类的构造函数结束。  Java互操作性是你需要使用次构造函数的主要应用场景。但是有一个可能的场景:当你有多种方式来创建带有不同参数列表的类实例的时候。我们将会后续的4.4.2一节讨论一个例子,  我们已经讨论了如何定义有意义的构造函数。现在让我们把注意力转移到有意义的属性。

4.2.3 实现声明在接口中的属性

在Kotlin中,一个接口可以包含抽象属性声明。这有一个声明这样一个接口定义的例子:


interface User {
    val nickname: String
}

这意味着实现了User接口的类需要提供一种方式来和获取nickname的值。接口并没有指定一个是否应该存储在一个备份字段或者通过一个getter来获取。因此,接口本身不包含任何状态。如果有需要的话,也只有实现了接口的类能够存储值。  让我们来看看接口的一些可能的实现:PrivateUser,只填充他们的昵称;SubscribingUser,被迫提供一个邮箱来注册;FacebookUser,简单的共享了他们的Facebook账号ID。所有的这些类都以不同的方式实现了接口中的抽象属性:


class PrivateUser(override val nickname: String) : User // 1 主构造函数属性

class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@') // 2 自定义getter
属性初始化器
}

class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId) // 3 
属性初始化器
}

>>> println(PrivateUser("[email protected]").nickname)
[email protected]
>>> println(SubscribingUser("[email protected]").nickname)
test

对于PrivateUser,你使用精简的语法来直接声明一个主构造函数中的属性。这个属性实现了来自User类的抽象属性,因此你把它标记为override。  对于SubscribingUsernickname属性通过一个自定义的getter来实现。这个属性并没有支持字段(backing field)来存储它的值。它只有一个计算来自每个调用中的邮箱的昵称的getter。  对于FacebookUser,你可以在它的初始化器中为nickname属性分配值。你使用一个支持返回给定IDFacebook用户名称的getFacebookName函数(假设它在某个地方被定义了)。这个函数是代价昂贵的:他需要跟Facebook建立一个连接来得期望的数据。这就是为什么你决定在初始化阶段调用它一次。  注意nicknameSubscribingUserFacebookUser中的不同实现。尽管它们看上去很相似,第一个属性有一个在每次访问时计算substringBefore的自定义的getter,然而FacebookUser中的属性有一个存储类初始化时计算的结果的支持字段。  除了抽象属性声明之外,接口也可以包含带有多个getter和setter属性,只要它们不引用支持字段(支持字段要求在一个接口中存储状态,而这是不允许的)。 然我们来看一个例子:


interface User {
    val email: String
    val nickname: String
        get() = email.substringBefore('@') // 1 属性并没有支持字段:每次调用都要重新计算结果
}

这个字段包含了抽象属性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()) // 1 读取支持字段的值
更新支持字段的值

        field = value // 2 更新支持字段的值
    }
}

>>> 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 // 1 你无法在类的外部改变这个属性

    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。

SIDEBAR 更多有关属性的话题 在书的后续部分,我们将会继续属性的讨论。以下是一些参考:

  • 非空属性中的lateinit修饰符指的是,这个属性会在后续时机被初始化。在构造函数被调用之后初始化,在某些框架中是非常常见的。这个特性将会在第6章覆盖到。
  • 懒初始化属性,作为更加广泛的委托属性特性的一部分,将会在第7章被覆盖到。
  • 出于Java框架的兼容性考虑,你可以在Kotlin中使用模拟Java特性的标注。举个例子,属性的@JvmFielld标注无需使用访问器就暴露了一个public字段。你将会在第10章了解到更多有关标注的知识。
  • const修饰符让使用标注更加方便。同时它允许你使用一个原始类型或者String类型的属性作为标注声明。第10章会给出细节。

以上内容总结了我们有关如何在Kotlin编写重要构造器和属性的讨论。接下来,你将会看到如何使用data类的概念来让值-对象类变得更加友好。

results matching ""

    No results matching ""