前言
项目地址https://github.com/laibinzhi/KotlinDelegation
类委托
首先从类委托开始
1 | interface IWork { |
1 | 打印: |
以上的代码当中,我们定义了一个 IWork 接口,它的 work() 方法用于表示该工作,Teacher 和 Police 都实现了这个接口。接着,我们的 Tony 也实现了这个接口,同时通过 by 这个关键字,将接口的实现委托给了它的参数 work。这种委托模式在我们的实际编程中十分常见,Tony 相当于一个壳,它虽然实现了 IWork 这个接口,但并不关心它怎么实现。具体是用 Teacher 还是 Police,传不同的委托对象进去,它就会有不同的行为。另外,以上委托类的写法,等价于以下 Java 代码,我们可以再进一步来看下:
1 | class Tony implements IWork { |
以上代码显示,work() 将执行流程委托给了传入的 work 对象。所以说,Kotlin 的委托类提供了语法层面的委托模式。通过这个 by 关键字,就可以自动将接口里的方法委托给一个对象,从而可以帮我们省略很多接口方法适配的模板代码。
另外有个问题,如果Tony这个类自己也实现IWork接口,那执行结果会是怎样?
1 | class Tony(work: IWork) : IWork by work{ |
1 | 打印: |
也就是说自己也提供接口的实现,会优先使用自己的实现。
委托的原理
by关键字后面的对象实际上会被存储在类的内部,编译器则会父接口中的所有方法实现出来,并且将实现转移给委托对象去执行。
属性委托
自定义属性委托
1.要实现自定义属性委托,必须要遵循kotlin的规则。
1 | class MyDelegate { |
1 | 打印: |
- 对于 var 修饰的属性,我们必须要有 getValue、setValue 这两个方法,同时,这两个方法必须有 operator 关键字修饰。对于val 修饰的属性,只需要有getValue方法即可
- 我们的 str 属性是处于 PropertyDelegationTest02 这个类当中的,因此 getValue、setValue 这两个方法中的 thisRef 的类型,必须要是 PropertyDelegationTest02,或者是 PropertyDelegationTest02 的父类。也就是说,我们将 thisRef 的类型改为 Any 也是可以的。一般来说,这三处的类型是一致的,当我们不确定委托属性会处于哪个类的时候,就可以将 thisRef 的类型定义为“Any?”。
- 由于我们的 str 属性是 String 类型的,为了实现对它的委托,getValue 的返回值类型,以及 setValue 的参数类型,都必须是 String 类型或者是它的父类。大部分情况下,这三处的类型都应该是一致的。
2.借助 Kotlin 提供的 ReadWriteProperty、ReadOnlyProperty 实现自定义属性委托
1 | public fun interface ReadOnlyProperty<in T, out V> { |
如果我们需要为 val 属性定义委托,我们就去实现 ReadOnlyProperty 这个接口;如果我们需要为 var 属性定义委托,我们就去实现 ReadWriteProperty 这个接口。这样做的好处是,通过实现接口的方式,AS 可以帮我们自动生成 override 的 getValue、setValue 方法。
以前面的代码为例,我们的 MyDelegateV2,也可以通过实现 ReadWriteProperty 接口来编写:
1 | class MyDelegateV2 : ReadWriteProperty<PropertyDelegationTest02, String> { |
1 | 打印: |
延迟属性(懒加载属性)
懒加载,顾名思义,就是对于一些需要消耗计算机资源的操作,我们希望它在被访问的时候才去触发,从而避免不必要的资源开销。其实,这也是软件设计里十分常见的模式,我们来看一个例子:
1 | val lazyValue: Int by lazy { |
1 | 打印: |
通过“by lazy{}”,我们就可以实现属性的懒加载了。这样,通过上面的执行结果我们会发现:main() 函数的第一行代码,由于没有用到 lazyValue,所以不会去加载,第二句调用了lazyValue,才会去加载,第三句代码,之前已经获取了lazyValue的值,所以不会重新获取,直接返回。
换句话说:属性只有第一次被访问的时候才会去计算,之后则会将之前的计算结果缓存起来供后续使用
延迟属性原理
lazy函数其实是一个高阶函数
1 | public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer) |
我们现在以SynchronizedLazyImpl为例子:
1 | private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable { |
可以看到,lazy() 函数可以接收一个 LazyThreadSafetyMode 类型的参数,如果我们不传这个参数,它就会直接使用 SynchronizedLazyImpl 的方式。
- SYNCHRONIZED:默认情况下,延迟属性的计算是同步的:值只会在一个线程中得到计算,所有线程都会使用相同的的一个结果。(多线程同步,多线程安全)
- PUBLICATION:如果不需要初始化委托的同步,这样多个线程可以同时执行。(多线程不安全)
- NONE:如果确定初始化操作只会在一个线程中执行,这样会减少线程安全方面的开销。(多线程不安全)
非空属性
适用于无法在初始化阶段就确定属性值的场合,因为Kotlin要求对于类里面的每一个属性必须赋予初值,这个可以直接赋一个具体值,也可以通过init代码块来进行赋初值,但终归有一个地方要求赋初值,但是某些情况下没有办法在初始化的时候去确定初始值是什么,这个情况下随便赋一个没有意义的值,(空字符串是没有意义的,相信很多人这么做)。那现在我们可以使Delegates.notNull委托去实现。
我们翻看一下Delegates.notNull的源码
1 | public object Delegates { |
从代码上来看,如果在没有为这个属性赋值的情况下就去调用这个属性,就会抛出一个异常。例:
1 | /** |
1 | Exception in thread "main" java.lang.IllegalStateException: Property userName should be initialized before get. |
如果在调用之前,赋一个值给userName就不会抛异常了!!!
可观察属性
Delegates.observable
Delegates.observable 返回读取/写入属性的属性委托,该属性在更改时调用指定的回调函数。
1 | public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit): |
接收两个参数,参数initialValue表示属性的初始值,onChange表示属性更改后调用的回调。 调用此回调时,该属性的值已更改。onChange有三个参数,被赋值属性本身,旧的值,和新的值,接下来,我们看一下用法。例如:
1 | class Person { |
1 | 打印: |
Delegates.vetoable
另外,如果你想拦截改属性的话,可以使用vetoable函数。返回读取/写入属性的属性委托,该属性在更改时调用指定的回调函数,允许回调否决修改。
1 | public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean): |
同样,接收两个参数,initialValue表示熟悉的初始值,onChange,在尝试更改属性值之前调用的回调。 调用此回调时,该属性的值尚未更改。 如果回调返回true ,则将属性的值设置为新值,如果回调返回false ,则丢弃新值,并且属性保持其旧值。调用时机和Delegates.observable相反,接下来,我们看一下用法,例:
1 | class Person { |
1 | 打印: |
从结果可以看出,第一次获取level的值可以直接读取初始值10出来,然后第一次负责20,我们也可以从onChange得到值更改出来,并且新的值>=旧的值,所以允许修改,所以我们看到打印新的等级是20。然后我们尝试改一个新值比旧值小的数,发现level并没有修改成功。
map属性
将属性储存在map中,一种常见的应用场景就是将属性值存储到map中,用于json解析或者是一些动态行为,在这种情况下,您可以使用map实例本身作为委托属性的委托。
1 | class User(map: Map<String, Any?>) { |
注意:map中key的名字要与类中属性的名字保持一致,不然会报错
1 | Exception in thread "main" java.util.NoSuchElementException: Key name is missing in the map. |
提供委托(provideDelegate)
我们看一下StringDelegate这个例子,是最基础的属性委托。
1 | class StringDelegate(private var s: String = "Hello"): ReadWriteProperty<Owner, String> { |
我们希望 StringDelegate(s: String) 传入的初始值 s,可以根据委托属性的名字的变化而变化。我们应该怎么做?
实际上,要想在属性委托之前再做一些额外的判断工作,我们可以使用 provideDelegate 来实现。
1 | class StudentDelegator { |
1 | 打印: |
为了在委托属性的同时进行一些额外的逻辑判断,我们使用创建了一个新的 StudentDelegator,通过它的成员方法 provideDelegate 嵌套了一层,在这个方法当中,我们进行了一些逻辑判断,然后再把属性委托给 StringDelegate。
如此一来,通过 provideDelegate 这样的方式,我们不仅可以嵌套 Delegator,还可以根据不同的逻辑派发不同的 Delegator。
属性委托总结
ReadOnlyProperty和ReadWriteProperty
1 | 可用于实现只读属性的属性委托的基本接口。 |
1 | 可用于实现读写属性的属性委托的基本接口。 |
对于属性委托的要求
对于只读属性来说(val修饰的属性),委托需要提供一个名为getValue的方法,该方法接收如下参数:
- thisRef:需要是属性拥有者相同的类型或者是其父类型(对于拓展属性来说,这个类型指的是被拓展的那个类型)。
- property:需要的是KProperty<*>类型或者是其父类型。
对于getValue()方法需要返回与属性相同的类型或者是子类型
对于可变属性来说(var修饰的属性),委托需要提供一个名为setValue的方法,该方法要接受以下参数:
- thisRef:需要是属性拥有者相同的类型或者是其父类型(对于拓展属性来说,这个类型指的是被拓展的那个类型)。
- property:需要的是KProperty<*>类型或者是其父类型。
- new value:需要与属性的类型相同或者其父类型
getValue和setValue既可以作为委托类的成员方法实现,也可以成为其拓展方法来实现
这两个方法都要有operator关键字,对于委托类来说,他可以实现ReadWriteProperty、ReadOnlyProperty的接口,这两个接口包含了响应的getValue和setValue方法。
委托转换规则
对于每个委托属性来说,Kotlin编译器在底层会生成一个辅助的属性,然后将对原有属性的访问委托给这个辅助属性。
委托的实际运用
1.属性可见性封装
假设你有一个ArrayList的实例,可以恢复其最后删除的项目,基本上,你需要的就是和ArrayList一样的功能,同时还需要一个最后一个被移除元素的引用。一种方法就是集成自ArrayList,由于新的类是集成具体的ArrayList,而不是实现了MutableList接口,因此新的类的实现方式和ArrayList存在高度的耦合。那如果是覆盖remove方法,是不是一个好的方法?通过保留已删除item的引用,并委派MutableList的大部分空的实现给其他的对象。Kotlin的类委托就可以很好的实现。通过委托大多数工作给一个内部的ArrayList实例去实现,并且可以自定义它自己的行为。
1 | class ListWithTrash<T>(private val innerList: MutableList<T> = ArrayList()) : MutableList<T> by innerList { |
上面by关键字告诉Kotlin委托功能将会被一个内部名为innerList的ArrayList去实现代理,ListWithTrash仍然支持所有方法功能。通过提供桥接方法在可变MutableList界面中到内部ArrayList对象。还可以定义自己的行为。反编译看他的内部,其实还是会实现ArrayList的所有方法。
1 | package property_delegation; |
2.数据与 View 的绑定
在 Android 当中,如果我们要对“数据”与“View”进行绑定,我们可以用 DataBinding,不过 DataBinding 太重了,也会影响编译速度。其实,除了 DataBinding 以外,我们还可以借助 Kotlin 的自定义委托属性来实现类似的功能。这种方式不一定完美,但也是一个有趣的思路。这里我们以 TextView 为例:
1 |
|
以上的代码,我们为 TextView 定义了一个扩展函数 TextView.provideDelegate,而这个扩展函数的返回值类型是 ReadWriteProperty。通过这样的方式,我们的 TextView 就相当于支持了 String 属性的委托了。它的使用方式也很简单:
1 |
|
总结
- 委托类,委托的是接口的方法,它在语法层面支持了“委托模式”。
- 委托属性,委托的是属性的 getter、setter。虽然它的核心理念很简单,但我们借助这个特性可以设计出非常复杂的代码。
- 另外,Kotlin 官方还提供了几种标准的属性委托,它们分别是:两个属性之间的直接委托、by lazy 懒加载委托、Delegates.observable 观察者委托,以及 by map 映射委托;
- 两个属性之间的直接委托,它是 Kotlin 1.4 提供的新特性,它在属性版本更新、可变性封装上,有着很大的用处;
- by lazy 懒加载委托,可以让我们灵活地使用懒加载,它一共有三种线程同步模式,默认情况下,它就是线程安全的;Android 当中的 viewModels() 这个扩展函数在它的内部实现的懒加载委托,从而实现了功能强大的 ViewModel;
- 除了标准委托以外,Kotlin 可以让我们开发者自定义委托。
- 自定义委托,我们需要遵循 Kotlin 提供的一套语法规范,只要符合这套语法规范,就没问题;在自定义委托的时候,如果我们有灵活的需求时,可以使用 provideDelegate 来动态调整委托逻辑。