上一篇,我们学习了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类一样继续使用此类。 获取器和设置器是为我们生成的编译器。编译器会自动的从主构造函数中根据所有声明的属性提取以下函数:
如果这些函数在类中已经被明确定义了,或者从超类中继承而来,就不再会生成。
为了保证生成代码的一致性以及有意义,数据类需要满足以下条件:
请注意,对于那些自动生成的函数,编译器只使用在主构造函数内部定义的属性。如需在生成的实现中排除一个属性,请将其声明在类体中:
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函数可以按声明顺序获取数据类中的属性值,但不包括再类体中声明的属性。
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中的泛型。
老规矩,喜欢我的文章,欢迎素质三连:点赞,评论,关注,谢谢大家!