委托
将自己的事情交给别人做,自己只要关注这件事情处理的记过就好了,这就是委托的过程。
委托属性
属性委托通常是为了解决以下场景的问题
- 延迟属性(lazy property),其值只在首次访问时计算(首次访问时分配内存)
- 可观察属性(observable property),监听器或收到有关此属性变更的通知
- 把多个属性存储在一个映射(map)中,而不是每个存在单独的字段中
为了解决以上几种场景,Kotlin支持属性的委托,基本格式如下:
val/var 属性名:属性类型 by 表达式。在 by 后面的表达式是该 委托, 因为属性对应的
get()
(与set()
)会被委托给它的getValue()
与setValue()
方法。 属性的委托不必实现任何的接口,但是需要提供一个getValue()
函数(与setValue()
——对于 var 属性)。
委托实现的例子
1 | import kotlin.reflect.KProperty |
属性委托的要求:
thisRef
类型必须与属性所有者类型相同(或者是其超类型)(对于扩展属性的委托,必须与扩展属性所在类类型相同,或者是其超类型)- property类型必须是KProperty类型或其超类型
- value类型必须与属性类型相同或是其超类型
- getValue必须返回与属性类型的同的类型或其子类型
注意:lambda表达式返回值总是返回函数体内部最后一行表达式的值
延迟委托 lazy
使用示例:var propertyName by lazy lambda
lazy()
是接受一个 lambda 并返回一个 Lazy <T>
实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get()
会执行已传递给 lazy()
的 lambda 表达式并记录结果, 后续调用 get()
只是返回记录的结果。
lateinit 和 by lazy比较 (结果由 chatGPT提供)
1 | lateinit 和 by lazy 都是延迟初始化属性的方式,但它们有以下区别: |
可观察属性 Observable
主要实现是在Delegates.observable()中,例如:
1 | var test0: String by Delegates.observable("Randy") { property, oldValue, newValue -> println("${property.name}:$oldValue->$newValue") } |
输出:
1 | test0:Randy->Jack |
还可以通过Delegate.vetoable来防止属性被更改,例如:
1 | var test: String by Delegates.vetoable("ss") { property: KProperty<*>, oldValue: String, newValue: String -> |
当ss函数返回true是,表示允许修改test属性,反之则不允许修改
委托给另一个属性
从1.4起,支持将一个属性的getter和setter方法委托给另一个属性处理,这种委托对顶层和类的属性(成员属性或扩展属性)都可用。通常这种委托方式在 向后兼容 处理是会非常有用。例如:
1 | var topPro3: String = "ss" |
将属性存储在映射中
利用映射来存储 属性的值,非常适用于JSON解析这种场景
1 | class TestUser(var map: Map<String, Any?>) { |
局部属性委托
1 | fun example(computeFoo: () -> Foo) { |
上面的代码将局部属性 memoizedFoo 委托给了 computeFoo这个 lambda表达式,这样做的好处是 如果 if 条件不满足,memoizeFoo 根本就不会初始化
委托实现
委托实现就是通过使用组合的方式来代替通过继承的方式来进行一些业务逻辑的视线。
为什么要这样做呢?继承的缺点:
- 无法通过继承的方式,重用多个类的代码,(当然可以通过接口实现)
- 通过继承,子类必须无条件就收父类的内容,没得选
- 父类继承而来的实现是静态的,不能动态改变,虽然可以通过重写做一些改善,但还不够灵活
所以,后面就提出用组合的方式来取代通过继承来实现代码的复用。
闲话不多扯了,来看看Kotlin 中类委托实现:
1 | interface Base { |
同上上面这种写法,Kotlin编译后会自动生成 Base中定义的方法,同时对象b会存储在 Derived中,看看编译后 decompile来的Java代码
1 | public interface Base { |
可以看到,Derived 中最终处理 print的都是 $$delegate_0这个属性,这个属性就是通过by 指定的由Kotlin自动生成的。
属性
幕后字段
在Kotlin中, 如果属性至少一个访问器使用默认实现,那么Kotlin会自动提供幕后字段,用关键字field
表示,幕后字段主要用于自定义getter和setter中,并且只能在getter 和setter中访问。
幕后属性
对外表现为只读,对内表现为可读可写,我们将这个属性成为幕后属性。
参考文章:什么是幕后字段和幕后属性
SAM接口
只有一个单一抽象方法(但可以有多个非抽象的方法)的接口,这种借口可以直接用lambda函数来实现,从而使代码简洁
扩展
Kotlin 能够对一个类或接口扩展新功能而无需继承该类或者使用像装饰者这样的设计模式。 这通过叫做扩展的特殊声明完成。
扩展函数
声明一个扩展函数需用一个接收者类型也就是被扩展的类型来作为他的前缀。 下面代码为 MutableList<Int>
添加一个swap
函数:
1 | fun MutableList<Int>.swap(index1: Int, index2: Int) { |
扩展是静态的解析的
扩展不能真正的修改他们所扩展的类。通过定义一个扩展,并没有在一个类中插入新成员, 只不过是可以通过该类型的变量用点表达式去调用这个新函数。
扩展函数是静态分发的,即他们不是根据接收者类型的虚方法。 调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的。例如
1 | /** |
可空接收者
可以为可空的接收者定义扩展,这样的扩展可以在对象变量上调用(即使为空也可以调用),例如:
1 | fun main3() { |
输出结果:
1 | null |
扩展属性
可以为List扩展一个 lastIndex 属性,用来表示最后一个元素的下标
1 | val <T> List<T>.lastIndex: Int |
注意:
- 扩展属性不能有初始化器
- 要注意扩展属性定义的位置(大多数情况下都直接定义在包里)
kotlin单例实现
对象声明
1
2
3
4
5
6object Singleton {
val property: String = "I am a singleton"
fun method() {
println("Method of singleton")
}
}伴生对象
1
2
3
4
5
6
7
8class MyClass {
companion object {
val property: String = "I am a singleton"
fun method() {
println("Method of singleton")
}
}
}
伴生对象的扩展
可以直接用伴生对象所在的类的类名访问伴生对象的扩展方法和扩展属性
1 | class MyCompanion { |
扩展声明为成员
这个好像比较复杂
数据类
1 | /** |
生成的.class文件如下:
1 | public final data class User public constructor(id: kotlin.Int, name: kotlin.String) { |
密封类
1 | sealed class Error |
密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
密封类作为when的判断条件时,是不需要再最后写else判断的,因为密封类的值集合是受限的
嵌套类
普通类中定义的一个无特殊修饰关键字的类就成为嵌套类、嵌套接口
1 | class Outer { |
嵌套接口
1 | interface OuterInterface { |
内部类
类内部定义一个用 inner关键字修饰的类,这个inner关键字修饰的类就成为内部类,同Java一样,内部类会持有外部类的引用
1 | class Outer { |
匿名内部类
匿名内部类通常用对象表达式表示
1 | window.addMouseListener(object : MouseAdapter() { |
作用域函数
- let,扩展函数,调用let中使用的函数参数,并将调用let的对象作为参数,然后返回函数参数的结果
- with,非扩展函数,将上下文对象作为参数传递到lambda表达式中,在lambda表达式中可以同伙
this
来访问这个上下文对象 - run(非扩展形式的),就是开辟一个临时作用域来运行一段代码,并将运行代码的结果返回
- run(扩展形式的),将上下文对象传递到lambda表达式中,在lambda表达式中可以通过
this
来访问这个对象,同时将lambda的结果返回 - apply,扩展函数,对上下文对象应用一些列操作,并将对象本身返回
- also,扩展函数,可以理解为在做某个操作时顺带做下某事,上下文对象会作为参数用(it)表示,传递到lambda中,并最终在lambda执行后返回上下文对象本身
takeIf
和takeUnless
作为作用域函数的补充,这两个函数配合作用域函数使得可以作用域函数调用链中进行值的检查与筛选
takeIf,如果满足给定的条件,就返回对象本身,否则返回null
takeUnless,和takeIf相反,如果不满足,就返回对象本身,否则返回null
其他
相等判断
- 构成相等使用
==
运算符,并调用equals()
来确定两个实例是否相等。 - 引用相等使用
===
运算符,以检查两个引用是否指向同一对象。 - ?,左边调用对象如果为空,则返回null,不为空正常执行(空安全调用)
- !!,左边对象如果为空,则抛出NPE,否则正常执行(确定不为空)
?:,elvis 表达式,例子
1
2
3var firstName : String?
...
println(firstName?:"Unknown")// firstName 不为空则输出firstName,为空则输出Unknown
默认参数于具名参数
假如定义了以下数据类:
1 | data class User(var firstName: String? = null, var lastName: String?) |
那么,主构造函数中的 firstName
参数就是默认参数,具体使用例子如下:
1 | var user1 = User("randy")// 这种写法是错误,由于默认参数将居于未设默认值的参数之前,使用时必须 |