1. 程式人生 > >4.2 宣告一個帶有精簡的建構函式或者屬性的類

4.2 宣告一個帶有精簡的建構函式或者屬性的類

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關鍵字: 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() 關鍵字呼叫了對應的超類建構函式。下圖解釋了這一點。一個箭頭展示委託了那個建構函式。
委託建構函式

就像Java那樣,你也有一個可選的權利使用 this() 關鍵字從一個建構函式呼叫你的類中的其他建構函式。以下是它如何工作的:

class MyButton : View {
    constructor(ctx: Context) : this(ctx, MY_STYLE) { //  委託給類中的其他建構函式
// ...
    }

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

你可以改變 MyButton 類,來將其中一個建構函式委託給類中的其他建構函式,併為引數傳遞了預設值。如下圖所示,次建構函式繼續呼叫 super() 。
次建構函式繼續呼叫 super

如果類沒有主建構函式,那麼每一個次建構函式必須初始化基類或者委託其他做這些事的建構函式。

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("[email protected]").nickname)
[email protected]
>>> println(SubscribingUser("[email protected]").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 類的概念來讓值-物件類變得更加友好。