1. 程式人生 > 實用技巧 >Kotlin學習筆記 資料類與密封類

Kotlin學習筆記 資料類與密封類

資料類

Kotlin 可以建立一個只包含資料的類,關鍵字為data:

data class User(val name: String, val age: Int)

編譯器會自動的從主建構函式中根據所有宣告的屬性提取以下函式:

  • equals()/hashCode()
  • toString()格式如"User(name=John, age=42)"
  • componentN() functions對應於屬性,按宣告順序排列
  • copy()函式

如果這些函式在類中已經被明確定義了,或者從超類中繼承而來,就不再會生成。

為了保證生成程式碼的一致性以及有意義,資料類需要滿足以下條件:

  • 主建構函式至少包含一個引數。

  • 所有的主建構函式的引數必須標識為val或者var;

  • 資料類不可以宣告為abstract,open,sealed或者inner;

  • 資料類不能繼承其他類 (但是可以實現介面)。

複製

複製使用 copy() 函式,我們可以使用該函式複製物件並修改部分屬性, 對於上文的 User 類,其實現會類似下面這樣:

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

例項

使用 copy 類複製 User 資料類,並修改 age 屬性:

data class User(val name: String, val age: Int)


fun main(args: Array<String>) {
    val jack = User(name = "Jack", age = 1)
    val olderJack = jack.copy(age = 2)
    println(jack)
    println(olderJack)

}

輸出結果為:

User(name=Jack, age=1)
User(name=Jack, age=2)

資料類以及解構宣告

元件函式允許資料類在解構宣告中使用:

val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // prints "Jane, 35 years of age"

標準資料類

標準庫提供了Pair和Triple。在大多數情形中,命名資料類是更好的設計選擇,因為這樣程式碼可讀性更強而且提供了有意義的名字和屬性。


密封類

密封類用來表示受限的類繼承結構:當一個值為有限幾種的型別, 而不能有任何其他型別時。在某種意義上,他們是列舉類的擴充套件:列舉型別的值集合 也是受限的,但每個列舉常量只存在一個例項,而密封類 的一個子類可以有可包含狀態的多個例項。

宣告一個密封類,使用sealed修飾類,密封類可以有子類,但是所有的子類都必須要內嵌在密封類中。

sealed 不能修飾 interface ,abstract class(會報 warning,但是不會出現編譯錯誤)

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

fun eval(expr: Expr): Double = when (expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}

使用密封類的關鍵好處在於使用 when 表示式 的時候,如果能夠 驗證語句覆蓋了所有情況,就不需要為該語句再新增一個 else 子句了。

fun eval(expr: Expr): Double = when(expr) {
    is Expr.Const -> expr.number
    is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
    Expr.NotANumber -> Double.NaN
    // 不再需要 `else` 子句,因為我們已經覆蓋了所有的情況
}

我的理解密封類就是一種專門用來配合 when 語句使用的類,舉個例子,假如在 Android 中我們有一個 view,我們現在想通過 when 語句設定針對 view 進行兩種操作:顯示和隱藏,那麼就可以這樣做:

sealed class UiOp {
    object Show: UiOp()
    object Hide: UiOp()
} 
fun execute(view: View, op: UiOp) = when (op) {
    UiOp.Show -> view.visibility = View.VISIBLE
    UiOp.Hide -> view.visibility = View.GONE
}

以上功能其實完全可以用列舉實現,但是如果我們現在想加兩個操作:水平平移和縱向平移,並且還要攜帶一些資料,比如平移了多少距離,平移過程的動畫型別等資料,用列舉顯然就不太好辦了,這時密封類的優勢就可以發揮了,例如:

sealed class UiOp {
    object Show: UiOp()
    object Hide: UiOp()
    class TranslateX(val px: Float): UiOp()
    class TranslateY(val px: Float): UiOp()
}

 

fun execute(view: View, op: UiOp) = when (op) {
    UiOp.Show -> view.visibility = View.VISIBLE
    UiOp.Hide -> view.visibility = View.GONE
    is UiOp.TranslateX -> view.translationX = op.px // 這個 when 語句分支不僅告訴 view 要水平移動,還告訴 view 需要移動多少距離,這是列舉等 Java 傳統思想不容易實現的
    is UiOp.TranslateY -> view.translationY = op.px
}

以上程式碼中,TranslateX 是一個類,它可以攜帶多於一個的資訊,比如除了告訴 view 需要水平平移之外,還可以告訴 view 平移多少畫素,甚至還可以告訴 view 平移的動畫型別等資訊,我想這大概就是密封類出現的意義吧。

除此之外,如果 when 語句的分支不需要攜帶除“顯示或隱藏view之外的其它資訊”時(即只需要表明 when 語句分支,不需要攜帶額外資料時),用 object 關鍵字建立單例就可以了,並且此時 when 子句不需要使用 is 關鍵字。只有需要攜帶額外資訊時才定義密封類的子類,而且使用了密封類就不需要使用 else 子句,每當我們多增加一個密封類的子類或單例,編譯器就會在 when 語句中給出提示,可以在編譯階段就及時發現錯誤,這也是以往 switch-case 語句和列舉不具備的功能。

最後,我們甚至可以把這一組操作封裝成一個函式,以便日後呼叫,如下:

// 先封裝一個UI操作列表
class Ui(val uiOps: List = emptyList()) {
    operator fun plus(uiOp: UiOp) = Ui(uiOps + uiOp)
}

// 定義一組操作
val ui = Ui() +
        UiOp.Show +
        UiOp.TranslateX(20f) +
        UiOp.TranslateY(40f) +
        UiOp.Hide
// 定義呼叫的函式
fun run(view: View, ui: Ui) {
    ui.uiOps.forEach { execute(view, it) }
}

run(view, ui) // 最終呼叫