Kotlin學習筆記12——數據類和密封類

前言

上一篇,我們學習了Kotlin中的拓展,今天繼續來學習Kotlin中的數據類和密封類。

數據類

我們經常創建一些只保存數據的類。 在這些類中,一些標準函數往往是從數據機械推導而來的。在 Kotlin 中,這叫做 數據類 並標記爲 data:

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

爲什麼要引入數據類?我們先看一個Java POJO(普通Java對象)

class Dog{ 
     private String name; 
     private String sound; 
     private String age; 
     public String getName() { 
         return name; 
     } 
     public void setName(String name) { 
         this .name = name; 
     } 
     public String getSound() { 
         return sound; 
     } 
     public void setSound(String sound) { 
         this .sound = sound; 
     } 
     public String getAge() { 
         return age; 
     } 
     public void setAge(String age) { 
         this .age = age; 
     }

所有這些儀式都用於一個簡單的Java POJO類,該類用於保存一些數據。 在對來自API的JSON響應進行序列化/反序列化時,您可能使用過此類。 它爲您提供屬性的獲取器和設置器。

現在,讓我們看看如何藉助數據類在Kotlin中完成此操作:

data class Dog(var name: String, var age: String, var sound: String) 

沒錯,只需一行! 我們可以像使用Java類一樣繼續使用此類。 獲取器和設置器是爲我們生成的編譯器。編譯器會自動的從主構造函數中根據所有聲明的屬性提取以下函數:

  • equals() / hashCode()
  • toString() 格式如 “Dog(name=John, age=42,sound=喵喵)”
  • componentN() functions 對應於屬性,按聲明順序排列
  • copy() 函數

如果這些函數在類中已經被明確定義了,或者從超類中繼承而來,就不再會生成。
爲了保證生成代碼的一致性以及有意義,數據類需要滿足以下條件:

  • 主構造函數至少包含一個參數。
  • 所有的主構造函數的參數必須標識爲val 或者 var ;
  • 據類不可以聲明爲 abstract, open, sealed 或者 inner;
  • 數據類不能繼承其他類 (但是可以實現接口)。

在類體中聲明的屬性

請注意,對於那些自動生成的函數,編譯器只使用在主構造函數內部定義的屬性。如需在生成的實現中排除一個屬性,請將其聲明在類體中:

data class Person(val name: String) {
    var age: Int = 0
}

在 toString()、 equals()、 hashCode() 以及 copy() 的實現中只會用到 name 屬性,並且只有一個 component 函數 component1(),且這個component1返回的是name的值。

複製

複製使用 copy() 函數,我們可以使用該函數複製對象並修改部分屬性, 其實現會類似下面這樣:

data class Cat(var name: String, var age: String, var sound: String)

var cat = Cat("Jim","2","喵喵")
var catTom = cat.copy(name="Tom")
println("catTom'name is ${catTom.name}")
//打印結果:catTom'name is Tom

componentN

使用數據類對象調用componentN函數可以按聲明順序獲取數據類中的屬性值,但不包括再類體中聲明的屬性。

data class Cat(var name: String, var age: String, var sound: String){
	//類體中聲明屬性
	var height:Int = 30
}

var cat = Cat("Jim","2","喵喵")
var catTom = cat.copy(name="Tom")
println("catTom'name is ${catTom.name} component1 : ${cat.component1()} component2 : ${cat.component2()} component3 : ${cat.component3()}")
//打印結果:catTom'name is Tom component1 : Jim component2 : 2 component3 : 喵喵

解構聲明

組件函數允許數據類在解構聲明中使用:

var cat = Cat("Jim","2","喵喵")
val (name,age,sound) = cat
println("$name $age $sound")
//打印結果:Jim 2 喵喵

有時把一個對象 解構 成很多變量會很方便:

val (name,age,sound) = cat

這種語法稱爲 解構聲明 。一個解構聲明同時創建多個變量。 我們已經聲明瞭兩個新變量: name 和 age,sound並且可以獨立使用它們:

...
println("$name $age $sound")
...

以上等價於:

...
println("${cat.component1()} ${cat.component2()} ${cat.component3()}")
...

密封類

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

聲明一個密封類,使用 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 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關鍵字對+進行重載
    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) // 最終調用

尾巴

今天的學習筆記就先到這裏了,下一篇我們將學習Kotlin中的泛型
老規矩,喜歡我的文章,歡迎素質三連:點贊,評論,關注,謝謝大家!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章