跳到主要内容

Kotlin委托(属性代理)

这篇文章一起来看下 Kotlin 委托属性代理。属性代理可以说是 Kotlin 独有的强大的功能之一,特别是对于框架开发的小伙伴来说非常有用,因为会经常涉及到更改存储和修改属性的方式操作,例如 Kotlin 中的 SQL 框架 Exposed 源码就大量使用了属性代理。相信你已经在代码也使用了诸如Delegates.observable()、Delegates.notNull()、Delegates.vetoable()或者自定义的属性代理。

1. 属性代理的基本定义

1.1 基本定义

属性代理是借助于代理设计模式,把这个模式应用于一个属性时,它可以将访问器的逻辑代理给一个辅助对象。

可以简单理解为属性的 setter、getter 访问器内部实现是交给一个代理对象来实现,相当于使用一个代理对象来替换了原来简单属性字段读写过程,而暴露外部属性操作还是不变的,照样是属性赋值和读取,只是 setter、getter 内部具体实现变了。

1.2 基本语法格式

class Student{
var name: String by Delegate()
}

class Delegate{
operator fun <T> getValue(thisRef: Any?, property: KProperty<\*>): T{
...
}
operator fun <T> setValue(thisRef: Any?, property: KProperty<\*>, value: T){
...
}
}

属性 name 将它访问器的逻辑委托给了 Delegate 对象,通过 by 关键字对表达式 Delegate() 求值获取这个对象。任何符合属性代理规则都可以使用 by 关键字。属性代理类必须要遵循 getValue(),setValue()方法约定,getValue、setValue方法可以是普通方法也可以是扩展方法,并且是方法是支持运算符重载。如果是 val 修饰的属性只需要具备 getValue() 方法即可。

属性代理基本流程就是代理类中的 getValue() 方法包含属性getter访问器的逻辑实现,setValue()方法包含了属性setter访问器的逻辑实现。当属性 name 执行赋值操作时,会触发属性 setter 访问器,然后在setter 访问器内部调用 delegate 对象的 setValue() 方法;执行读取属性 name 操作时,会在 getter 访问器中调用 delegate 对象的 getValue 方法.

1.3 by 关键字

by 关键字实际上就是一个属性代理运算符重载的符号,任何一个具备属性代理规则的类,都可以使用by 关键字对属性进行代理。

2. 常见属性代理基本使用

属性代理是 Kotlin 独有的特性,我们自己去自定义属性代理,当然 Kotlin 还提供了几种常见的属性代理实现。例如:Delegates.notNull(), Delegates.observable(), Delegates.vetoable()。

2.1 Delegates.notNull() 的基本使用

Delegate.notNull() 代理主要用于可以不在构造器初始化时候初始化而是可以延迟到之后再初始化这个var 修饰的属性,它和 lateinit 功能类似,但是也有一些不同,不过它们都需要注意的一点是属性的生命周期,开发者要做到可控,也就是一定要确保属性初始化是在属性使用之前,否则会抛出一个IllegalStateException。

package com.imooc.kotlin.delegate

import kotlin.properties.Delegates

class Teacher {
var name: String by Delegates.notNull()
}
fun main(args: Array<String>) {
val teacher = Teacher().apply { name = "Mikyou" }
println(teacher.name)
}

可能有的人并没有看到 notNull() 有什么大的用处

在 Kotlin 开发中与 Java 不同的是在定义和声明属性时必须要做好初始化工作,否则编译器会提示报错的,不像 Java 只要定义就 OK 了,管你是否初始化呢。

我解释下这也是 Kotlin 优于 Java 地方之一,没错就是空类型安全,就是 Kotlin 在写代码时就让你明确一个属性是否初始化,不至于把这样的不明确定义抛到后面运行时。如果在 Java 你忘记了初始化,那么恭喜你在运行时你就会拿到空指针异常。

相比 Java,Kotlin 属性定义时多出了额外的属性初始化的工作。但是可能某个属性的值在开始定义的时候你并不知道,而是需要执行到后面的逻辑才能拿到。这时候解决方式大概有这么几种:

  • 方式A:开始初始化的时给属性赋值个默认值
  • 方式B:使用Delegates.notNull()属性代理
  • 方式C:使用lateinit修饰属性

以上三种方式有局限性,方式 A 就是很直接赋默认值,对于基本类型还可以,但是对于引用类型的属性,赋值一个默认引用类型对象就感觉不太合适了。方式 B 适用于基本数据类型和引用类型,但是存在属性初始化必须在属性使用之前为前提条件。方式 C 仅仅适用于引用类型,但是也存在属性初始化必须在属性使用之前为前提条件。

优缺点分析:

属性使用方式优点缺点
方式A(初始化赋默认值)使用简单,不存在属性初始化必须在属性使用之前的问题仅仅适用于基本数据类型
方式B(Delegates.notNull()属性代理)适用于基本数据类型和引用类型
2、不支持外部注入工具将它直接注入到Java字段中
方式C(lateinit修饰属性)仅适用于引用类型1、存在属性初始化必须在属性使用之前的问题;
2、不支持基本数据类型

Tips: 如果能对属性生命周期做很好把控的话,且不存在注入到外部字段需求,建议使用方式 B;此外还有一个不错建议就是方式 A+ 方式 C组合,或者方式 A+ 方式 B 组合。具体看实际场景需求。

2.2 Delegates.observable()的基本使用

Delegates.observable()主要用于监控属性值发生变更,类似于一个观察者。当属性值被修改后会往外部抛出一个变更的回调。它需要传入两个参数,一个是 initValue 初始化的值,另一个就是回调 lamba, 回调出 property, oldValue, newValue 三个参数。

package com.imooc.kotlin.delegate

import kotlin.properties.Delegates

class Person{
var address: String by Delegates.observable(initialValue = "NanJing", onChange = {property, oldValue, newValue ->
println("property: ${property.name} oldValue: $oldValue newValue: $newValue")
})
}

fun main(args: Array<String>) {
val person = Person().apply { address = "ShangHai" }
person.address = "BeiJing"
person.address = "ShenZhen"
person.address = "GuangZhou"
}

运行结果:

property: address  oldValue: NanJing  newValue: ShangHai
property: address oldValue: ShangHai newValue: BeiJing
property: address oldValue: BeiJing newValue: ShenZhen
property: address oldValue: ShenZhen newValue: GuangZhou
Process finished with exit code 0

2.3 Delegates.vetoable()的基本使用

Delegates.vetoable()代理主要用于监控属性值发生变更,类似于一个观察者,当属性值被修改后会往外部抛出一个变更的回调。它需要传入两个参数,一个是initValue初始化的值,另一个就是回调lamba, 回调出property, oldValue, newValue三个参数。与observable不同的是这个回调会返回一个Boolean值,来决定此次属性值是否执行修改。

package com.imooc.kotlin.delegate

import kotlin.properties.Delegates

class Person{
var address: String by Delegates.vetoable(initialValue = "NanJing", onChange = {property, oldValue, newValue ->
println("property: ${property.name} oldValue: $oldValue newValue: $newValue")
return@vetoable newValue == "BeiJing"
})
}

fun main(args: Array<String>) {
val person = Person().apply { address = "NanJing" }
person.address = "BeiJing"
person.address = "ShangHai"
person.address = "GuangZhou"
println("address is ${person.address}")
}

3. 常见属性代理的源码分析

以上我们介绍了常见的属性代理基本使用,如果仅仅停留在使用的阶段,确实有点low了, 那么让我们一起先来揭开它们的第一层外衣。先来看波Kotlin标准库源码中常见的属性代理包结构。

3.1 源码包结构

图片描述

3.2 关系类图

图片描述

  • Delegates : 是一个代理单例对象,里面有 notNull、observable、vetoable 静态方法,每个方法返回不同的类型代理对象;
  • NotNullVar : notNull 方法返回代理对象的类;
  • ObserableProperty : observable、vetoable方法返回代理对象的类;
  • ReadOnlyProperty : 只读属性代理对象的通用接口;
  • ReadWriteProperty : 读写属性代理对象的通用接口。

3.3 Delegates.notNull()源码分析

notNull() 首先是一个方法,返回的是一个 NotNullVar 属性代理实例;那么它处理核心逻辑就是NotNullVar 内部的 setValue 和 getValue 方法,一起来瞅一眼。

  public override fun getValue(thisRef: Any?, property: KProperty<\*>): T {
return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
}

public override fun setValue(thisRef: Any?, property: KProperty<\*>, value: T) {
this.value = value
}

通过源码可以看到一旦 getValue 中的 value 是为 null,那么就会抛出一个 IllegalStateException,也就是在使用该属性之前没有做初始化。实际上可以理解在访问器 getter 加了一层判空的代理实现。

3.4 Delegates.observable()源码分析

observable() 是一个方法,返回的是一个 ObservableProperty 属性代理实例;那它是怎么做到在属性值发生变化通知到外部的呢,其实很简单,首先在内部保留一个 oldValue 用于存储上一次的值,然后就在 ObservableProperty 类 setValue 方法执行真正赋值之后再向外部抛出了一个 afterChange 的回调,并且把 oldValue,newValue,property 回调到外部,最终利用 onChange 方法回调到最外层。

 public override fun setValue(thisRef: Any?, property: KProperty<\*>, value: T) {
val oldValue = this.value
if (!beforeChange(property, oldValue, value)) {
return
}
this.value = value
afterChange(property, oldValue, value)
}
public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<\*>, oldValue: T, newValue: T) -> Unit):
ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<\*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
}

3.5 Delegates.vetoable() 源码分析

vetoable() 是一个方法,返回的是一个 ObservableProperty 属性代理实例;通过上面源码就可以发现,在 setValue 方法中执行真正赋值之前,会有一个判断逻辑,根据 beforeChange 回调方法返回的 Boolean 决定是否继续执行下面的真正赋值操作。如果 beforChange() 返回 false 就终止此次赋值,那么 observable 也不能得到回调,如果返回 true 则会继续此次赋值操作,并执行 observable 的回调。

4. 属性代理背后的原理和源码反编译分析

如果说第三节是揭开属性代理第一层外衣,那么第四节将是揭开最后一层外衣了,你会看到属性代理真正背后的原理,看完你会发现其实挺简单的。不多说先上一个简单例子

class Teacher {
var name: String by Delegates.notNull()
var age: Int by Delegates.notNull()
}

实际上,以上那行代码是经历了两个步骤:

class Teacher {
private val delegateString: ReadWriteProperty<Teacher, String> = Delegates.notNull()
private val delegateInt: ReadWriteProperty<Teacher, Int> = Delegates.notNull()
var name: String by delegateString
var age: Int by delegateInt
}

Kotlin 反编译后 Java 源码:

public final class Teacher {
// $FF: synthetic field
//关键点一
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Teacher.class), "name", "getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Teacher.class), "age", "getAge()I"))};
//关键点二
@NotNull
private final ReadWriteProperty name$delegate;
@NotNull
private final ReadWriteProperty age$delegate;

//关键点三
@NotNull
public final String getName() {
return (String)this.name$delegate.getValue(this, $$delegatedProperties[0]);
}

public final void setName(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.name$delegate.setValue(this, $$delegatedProperties[0], var1);
}

public final int getAge() {
return ((Number)this.age$delegate.getValue(this, $$delegatedProperties[1])).intValue();
}

public final void setAge(int var1) {
this.age$delegate.setValue(this, $$delegatedProperties[1], var1);
}

public Teacher() {
this.name$delegate = Delegates.INSTANCE.notNull();
this.age$delegate = Delegates.INSTANCE.notNull();
}
}

分析过程:

  1. 首先, Teacher 类的 name 和 age 属性会自动生成对应的 setter,getter 方法,并且会自动生成对应的name$delegate、age$delegate 委托对象,如代码中标识的关键点二。
  2. 然后,$$delegatedProperties 的 KProperty 数组中会保存通过 Kotlin 反射出当前 Teacher 类中的中name,age 属性,反射出来每个属性单独对应保存在 KProperty 数组中。
  3. 然后,在对应属性 setter,getter 方法中是把具体的实现委托给对应的name$delegate、age$delegate 对象的 setValue、getValue 方法来实现的,如代码中标识的关键点三。
  4. 最后,在 delegate 对象中的 setValue 和 getValue 方法中的传入对应反射出来的属性以及相应的值。

5. 自定义属性代理

有以上的介绍,自己写个自定义的属性代理应该很简单了吧。实现一个简单的属性代理最基本架子就是 setValue,getValue 方法且无需实现任何的接口。

在 Android 中 SharedPreferences 实际上就是个很好场景,因为它涉及到了属性存储和读取。自定义属性代理实现 Android中SharedPreferences 可以直接实现自带的 ReadWriteProperty 接口,当然也可以自己去写一个类然后去定义相应的 setValue 方法和 getValue 方法。

class PreferenceDelegate<T>(private val context: Context, private val name: String, private val default: T, private val prefName: String = "default")
: ReadWriteProperty<Any?, T> {
private val prefs: SharedPreferences by lazy {
context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
}

override fun getValue(thisRef: Any?, property: KProperty<\*>): T {
println("setValue from delegate")
return getPreference(key = name)
}

override fun setValue(thisRef: Any?, property: KProperty<\*>, value: T) {
println("setValue from delegate")
putPreference(key = name, value = value)
}

private fun getPreference(key: String): T {
return when (default) {
is String -> prefs.getString(key, default)
is Long -> prefs.getLong(key, default)
is Boolean -> prefs.getBoolean(key, default)
is Float -> prefs.getFloat(key, default)
is Int -> prefs.getInt(key, default)
else -> throw IllegalArgumentException("Unknown Type.")
} as T
}

private fun putPreference(key: String, value: T) = with(prefs.edit()) {
when (value) {
is String -> putString(key, value)
is Long -> putLong(key, value)
is Boolean -> putBoolean(key, value)
is Float -> putFloat(key, value)
is Int -> putInt(key, value)
else -> throw IllegalArgumentException("Unknown Type.")
}
}.apply()

}

6. 总结

到这里属性代理的内容就结束了,有没有觉得 Kotlin 语言糖设计还是很巧妙的。虽然很多人抵触语法糖,但不可否认的是它给我们开发在效率上带来了很大的提升。有时候我们更多地是需要透过语法糖外衣,看到其背后的原理,弄清整个语法糖设计思路和技巧,应该对 Kotlin 属性扩展有了比较深的认识了。