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中的泛型
老规矩,喜欢我的文章,欢迎素质三连:点赞,评论,关注,谢谢大家!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值