Kotlin学习——类与对象(二)

扩展

Kotlin能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式。这通过叫做扩展的特殊声明完成。

例如,可以为一个不能修改的、来自三方的库中的类编写一个新的函数。这个新增的函数就像那个原始本来就有的函数一样,可以用普通的方法调用。这种机制成为扩展函数。此外,也有扩展属性,允许为一个已经存在的类添加新的属性

扩展函数

声明一个扩展函数,需要用一个接收者类型也就是被扩展的类型来作为它的前缀。

下面代码为 MutableList<Int> 添加一个 swap 函数:

fun MutableList<Int>.swap(index1: Int, index2: Int){
    val tmp = this[index1]  //"this"对应列表
    this[index1] = this[index2]
    this[index2] = tmp
}

//这个 this 关键字在扩展函数内部对应到接收者对象(传过来的在点符号之前的对象)
//调用如下
val list = mutableListOf(1,2,3)
//swap()内部中的 this 会保存 list 的值
list.swap(0,2)


//当然我们可以泛化这个函数
//为了在接收者类型表达式中使用泛型,我们要在函数名前声明泛型参数
fun <T> MutableList<T>.swap(index1: Int, index2: Int){
    val tmp = this[index1]
    this[index1] = this[index2]
    this[index2] = tmp
}

扩展是静态解析的:扩展并不能真正修改他们所扩展的类。通过定义一个扩展,并没有在一个类中插入新成员,仅仅是可以通过该类型的变量用点表达式去调用这个新函数。想强调的是扩展函数是静态分发的,即他们不是根据接收者类型的虚方法,这意味着调用的扩展函数是由函数调用所在的表达式的类型来决定的,而不是由表达式运行时求值结果决定的

如果一个类定义有一个成员函数与一个扩展函数,而这两个函数又有相同的行参类型、相同的名字,并且都适用于给定的参数,这种情况总是取成员函数

可空接收者:注意可以为可空的接受者类型定义扩展。这样的扩展可以在对象变量上调用,即使其值为null,并且可以在函数内检测 this==null,这能让在没有检测null的时候调用Kotlin中的toString():检测发生在扩展函数的内部

fun Any?.toString(): String{
    if(this == null) return "null"
    //空检测之后,"this"会自动转换为非空类型,所以下面的 toString()解析为Any类的成员函数
    return toString()
}

扩展属性

与函数类似,由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。所以扩展属性不能有初始化器,他们的行为只能由显示提供的 getters/setters 定义

伴生对象的扩展

如果一个类定义有一个伴生对象,也可以为伴生对象定义扩展函数与属性。就像伴生对象的常规成员一样,可以只使用类名作为限定符来调用伴生对象的扩展成员

class MyClass{
    companion object{}  //伴生对象,将被称为 “Companion”
}

fun MyClass.Companion.printCompanion(){ println("companion")}

扩展的作用域

大多数时候我们在顶层定义扩展——直接在包里

要使用定义包之外的一个扩展,我们需要在调用方导入它

扩展声明为成员

在一个类内部你可以为另一个类声明扩展。在这样的扩展内部,有多个隐式接受者——其中的对象成员可以无需通过限定访问符访问。扩展声明所在的类的实例称为分发接收者,扩展方法调用所在的接收者类型的实例称为扩展接收者

数据类

我们经常创建一些只保存数据的类,在这些类中,一些标准函数往往是从数据机械推导而来的。在Kotlin中,这叫做数据类并标记为 data:

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

为了确保生成的代码一致性以及有意义的行为,数据类必须满足以下要求:

  • 主构造函数需要至少一个参数
  • 主构造函数所有的参数需要标记为 val 或 var
  • 数据类不能是抽象、开放、密封或者内部的
  • (在1.1之前)数据类只能实现接口

此外,成员生成遵循关于成员继承的这些规则

  • 如果在数据类体中有显式实现 equals()、hashCode()或者toString(),或者这些函数在父类中有final实现,那么不会生成这些函数,而会使用现有函数
  • 如果父类型就有 open 的 componentN() 函数并且返回兼容的类型,那么会为数据类生成相应的函数,并覆盖父类的实现。如果父类型的这些函数由于签名不兼容或者是final而导致无法覆盖,那么会报错
  • 从一个已具 copy(......) 函数且签名匹配的类型派生一个数据类在Kotlin1.2中已弃用,并且在Kotlin1.3中已禁用
  • 不允许为 componentN() 以及 copy() 函数提供显式实现

在 JVM 中,如果生成的类需要含有一个无参的构造函数,则所有的属性必须指定默认值

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

复制

很多情况下,我们需要复制一个对象改变它的一些属性,但是其余部分保持不变,copy() 函数就是为此而生成的

//在User类中,增加copy方法
fun copy(name: String = this.name, age: Int =this.age) = User(name, age) 


val jack = User(name = "jack", age = 1)
val oldJack = jack.copy(age = 2)

数据类与解构声明

为数据类生成的 Component 函数使它们可在解构声明中使用

val cindy = User("Cindy", 26)
val (name, age) = cindy
//输出 "cindy, 26 years of age"
println("$name, $age years of age")

密封类

密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例

要声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明

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

一个密封类是自身抽象的,它不能直接实例化并可以有抽象成员

密封类不能有非 private 构造函数(其构造函数默认为 private)

使用密封类的好处在于使用 when 表达式的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句,当然只有当使用when作为表达式(使用结果)而不是作为语句时才有用

fun eval(expr: Expr): Double = when(expr){
    is Const -> expr.number
    is Sum   -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
    //不需要else子句因为已经覆盖了所有的情况
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值