有种完成工作的方式就是把工作委派给其他人。当然我并非讨论的是,你将你的工作委派给你的朋友,而是将你的工作从一个对象委派到另外一个对象,质量有守恒定律,同样工作必须有人去完成。
你猜怎么着,委派在软件行业并不是一个新兴的模式。委派是23种设计模式中的一种,具体对象通过委派给一个助理对象来处理相关的请求。委派对象代表原始对象来相应请求的处理,并使得处理的结果被原始对象所使用。
Kotlin通过提供对类和属性的委派使得使用kotlin来创建委派时更容易、更简单和方便。甚至Kotlin还包含一些自己的内置委托。
类的委派
现在有个需求,一个 ArrayList 可以恢复它最后一个被删除的元素。基本上,所有你需要的就是和 ArrayList 一样的功能,同时还需要一个最后一个被移除元素的引用。
其中的一个做法就是继承 ArrayList。因为新的类是继承自具体的ArrayList,而不是实现了 MutableList 接口,因此新类的实现方式和 ArrayList 存在高度的耦合。
如果你可以直接覆盖掉 ArrayList 的 remove 方法,来新增一个被删除元素的引用,并委派 MutableList 的大部分空实现给其它的对象,这种方式岂不是更好么?Kotlin提供了一种方式来实现刚才的的思路,它就是通过委派大多数工作给一个内部的 ArrayList 实例来实现的,并且可以自定义它自己的行为。为了实现这个功能,Kotlin引入了一个新的关键字 : by .
我们来看看类委派是如何实现的。当使用 by 关键字时,Kotlin会自动生成使用 innerList 实例的代码来作为委派:
class ListWithTrash <T>(
private val innerList: MutableList<T> = ArrayList<T>()
) : MutableCollection<T> by innerList {
var deletedItem : T? = null
override fun remove(element: T): Boolean {
deletedItem = element
return innerList.remove(element)
}
fun recover(): T? {
return deletedItem
}
}
by 关键字告诉 Kotlin MutableList接口的功能将会被一个内部名为 innerList 的 ArrayList 实例代理。ListWithTrash 依然会通过内部 ArrayList 对象直接桥接方法的方式支持 MutableList 接口的所有功能。另外你还可以添加自己自定义的能力。
源码之下:
我们来看看这个是如何实现的。如果我们来查看 ListWithTrash 反编译之后的 Java 代码,你可以发现其实 Kotlin 为 ArrayList 调用的相关方法都创建了包装方法:
public final class ListWithTrash implements Collection, KMutableCollection {
@Nullable
private Object deletedItem;
private final List innerList;
@Nullable
public final Object getDeletedItem() {
return this.deletedItem;
}
public final void setDeletedItem(@Nullable Object var1) {
this.deletedItem = var1;
}
public boolean remove(Object element) {
this.deletedItem = element;
return this.innerList.remove(element);
}
@Nullable
public final Object recover() {
return this.deletedItem;
}
public ListWithTrash() {
this((List)null, 1, (DefaultConstructorMarker)null);
}
public int getSize() {
return this.innerList.size();
}
// $FF: bridge method
public final int size() {
return this.getSize();
}
//...and so on
}
提示:在自动生成的代码中,Kotlin 编辑器其实使用了另外一种称之为装饰模式的设计模式来支持了 Kotlin 的委派功能。在装饰模式中,装饰类和被装饰类共享了同一套接口。装饰类保留目标类的引用,并包装或者装饰该接口提供的所有的共有方法。
当你不能继承特定的类对象时,委派将会非常的有用。通过类委派,虽然你的类对象不是任何类的继承关系的一部分。但是它共享了相同的接口,并装饰了原始类型的内部对象。这就意味着你在不破坏公有的 API 的情况下,很容易去改变相关的实现。
委派属性
除了类的委派外,你也可以使用 by 关键字来委派属性。通过属性委派来负责对属性的get和set方法进行调用。当你需要在不同的对象上重用 getter/setter 方法时,这可能非常的有用。通过简单的后备字段就可以很容易地扩展字段。
来想想你有一个 Person 类定义如下:
class Person (var name: String, var lastname: String)
name 属性需要一些格式化的需求。name 设置如下:你想首字母大写,其余剩下的字母小写。当然,当更新 name 时,你想自动地增加更新属性次数的记录。
那么你可能会这么去写:
class Person(name: String, var lastname: String) {
var name: String = name
set(value) {
name = value.toLowerCase().capitalize()
updateCount++
}
var updateCount = 0
}
当然了 这很有效果。但是如果当你的需求改变了,当你的 lastname 改变时也每次增加 updataCount 的次数。你可能会 复制 name set的逻辑,但是你会发现你为每个属性都写了同样一套setter方法:
class Person(name: String, lastname: String) {
var updateCount = 0
var name: String = name
set(value) {
name = value.toLowerCase().capitalize()
updateCount++
}
var lastname: String = lastname
set(value) {
lastname = value.toLowerCase().capitalize()
updateCount++
}
}
两个 setter 方法基本上都相同,我告诉你其中一个没有必要存在。通过属性的委托,你可以通过委托 getter 和 setter 方法来实现代码的复用。
就像 类委托一样,你可以使用 by 关键字来委托一个属性。而且 Kotlin 会自动生成代码来委托你的属性。
class Person(name: String, lastname: String) {
var name: String by FormatDelegate()
var lastname: String by FormatDelegate()
var updateCount = 0
}
通过这种更改,你已经委托了 name 和 lastname 属性给 FormatDelegate 类了。现在我们来看一看 FormatDelegate 类长什么样。当你只需要委托 getter 方法时,这个委托类需要实现 ReadProperty<Any?,String> 接口,而当你需要委托 getter/setter 两个方法时,你需要实现 ReadWriteProperty<Any?,String> 接口了。在我们的当前情况下,FormatDeletegate 需要实现 ReadWriteProperty<Any?,String> 接口,因为你想当 setter方法被调用时,进行格式化。
class FormatDelegate : ReadWriteProperty<Any?, String> {
private var formattedString: String = ""
override fun getValue(
thisRef: Any?,
property: KProperty<*>
): String {
return formattedString
}
override fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: String
) {
formattedString = value.toLowerCase().capitalize()
}
}
你可能会注意到 setter 和 getter 方法上面有两个额外的参数. 第一个参数 thisRef 表示拥有这个属性的对象。thisRef 可以被用来访问对象本身,例如检查其他属性或者调用其他函数等等。第二个参数 KProperty<*> 可以被用来接近委托属性的元数据(这个以后咱们再讲).
回看我们的需求,我们可以使用 thisRef 来获取我们的 updateCount 属性并增加它。
override fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: String
) {
if (thisRef is Person) {
thisRef.updateCount++
}
formattedString = value.toLowerCase().capitalize()
}
源码之下:
为了理解委派属性是如何工作的,我们来看一看反编译之后的 Java 代码。Kotlin 编译器为 name 和 lastname 都生成了 私有的 FormatDelegate 引用,伴随着 setter/getter方法,你的添加的逻辑也被加入其中。
编译器同时也生成了 KProperty[] 在存储 委派的属性。如果你看看 name 属性自动生成的 gettter 和 setter 方法,name对应的 FormateDelegate 实例存储在索引0的位置,而 lastname 对应的实例存储在索引1位置。
public final class Person {
// $FF: synthetic field
static final KProperty[] $delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "name", "getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "lastname", "getlastname()Ljava/lang/String;"))};
@NotNull
private final FormatDelegate name$delegate;
@NotNull
private final FormatDelegate lastname$delegate;
private int updateCount;
@NotNull
public final String getName() {
return 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);
}
//...
通过这个把戏,任何调用者都可以使用常规属性语法来调用委托属性。
person.lastname = "Smith" // 调用自动生成的 setter 方法,增加 count 计数。
在Kotlin标准库中,Kotlin不仅支持委托,而且还内置了委托,将来我们会聊聊这方面的东西。
总结:
委托可以帮助你委托任务到其它对象,并且提供了更好的代码复用。Kotlin编译器生成的代码让你无缝使用委托。Kotlin通过 by关键字简单语法来委托一个属性或者一个类。在反编译源码之下,Kotlin编译器自动生成了所有必要的代码来支持委托,并没有暴露对公有 API 的更改。简单来说,Kotlin生成并维护了所有需要的样板代码来供我们使用委托,或者换句话说,你可以在Kotlin中委派自己的委派任务。