上篇文章我们了解了Kotlin中的接口委托,还可以使用by关键字委托属性。
使用属性委托,委托负责处理对属性的get和set函数的调用。如果您需要跨其他对象重用getter/setter逻辑,这可能非常有用,并允许您轻松扩展功能,而不仅仅是简单的支持字段。
属性委托
让我们假设你有一个Product类,它是这样定义的:
class Product(price: String, oldPrice: String)
该类的price
属性有一些格式化要求。设置price
时,要确保前缀统一添加¥符号。另外,在更新价格时,您希望自动增加更新计数属性。
你可以实现如下所示的功能:
class Product(price: String, oldPrice: String) {
var price: String = price
set(value) {
field = "¥$value"
updateCount++
}
var updateCount = 0
}
在这个过程中,如果需求改变了,oldPrice原价格也需要遵循这个规则怎么办?你可以复制/粘贴这个逻辑来编写一个自定义setter,但突然你会发现自己为每个属性编写相同的setter:
class Product(price: String, oldPrice: String) {
var price: String = price
set(value) {
field = "¥$value"
updateCount++
}
var oldPrice: String = oldPrice
set(value) {
field = "¥$value"
updateCount++
}
var updateCount = 0
}
这两个setter方法几乎相同,告诉您其中一个不应该存在。使用属性委托,我们可以通过将getter和setter委托给属性来复用代码。
就像类委托一样,您可以使用by来委托属性,当您使用属性语法时,Kotlin将生成使用委托的代码
class Product(price: String, oldPrice: String) {
var price: String by PriceDelegate()
var oldPrice: String by PriceDelegate()
var updateCount = 0
}
伴随这这种变化,你已经把price
属性和oldPrice
属性委托给了PriceDelegate
这个类了。我们来看下PriceDelegate这个类。
如果你只想委托getter,你的委托类需要实现ReadProperty<Any?, String>
;如果你getter和setter都需要委托,代理类需要实现ReadWriteProperty<Any?, String>
。
在我们的例子中,PriceDelegate需要实现ReadWriteProperty<Any?String>
,因为您希望在调用setter时执行格式统一。
class PriceDelegate : 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"
}
}
您可能已经注意到在getter和setter函数中有两个额外的参数。
第一个参数是thisRef
,表示包含该属性的对象。thisRef
可以用于访问对象本身,例如检查其他属性或调用其他类函数。
第二个参数是==KProperty<*>==,它可用于访问委托属性上的元数据。
回顾一下需求,让我们使用thisRef来访问并增加updateCount属性。
override fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: String
) {
if (thisRef is Product) {
thisRef.updateCount++
}
formattedString = "¥$value"
}
底层原理
我们来理解下这是如何工作的,让我们看一下反编译的Java代码。
PriceDelegate对象的price和oldPrice委托属性的私有引用,以及包含所添加的逻辑的getter /setter是由Kotlin编译器生成代码。
public final class Product {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Product.class, "price", "getPrice()Ljava/lang/String;", 0)), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Product.class, "oldPrice", "getOldPrice()Ljava/lang/String;", 0))};
@NotNull
private final PriceDelegate price$delegate;
@NotNull
private final PriceDelegate oldPrice$delegate;
private int updateCount;
@NotNull
public final String getPrice() {
return this.price$delegate.getValue(this, $$delegatedProperties[0]);
}
public final void setPrice(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.price$delegate.setValue(this, $$delegatedProperties[0], (String)var1);
}
//...
}
使用这个技巧,任何调用者都可以使用常规的属性语法访问委托的属性。
fun main(){
val product : Product = Product("10","15")
product.oldPrice = "20" // calls generated setter, increments count
println("Update count is ${product.updateCount}")
}
实际应用
同在在设置页面会有控制开关的需求,每一个开关会定义一个属性,以获取和设置开关的状态,这时这个状态我们通常的做法是通过SharedPreferences存储和读取持久化状态,这里通常可以考虑开关属性通过属性委托的形式,痛过SharedPreferences把存储和读取。
代码如下:
class PreferenceDelegate<T>(val context: Context, val name: String, private val default: T) :
ReadWriteProperty<Any?, T> {
val prefs: SharedPreferences by lazy {
context.getSharedPreferences("default", Context.MODE_PRIVATE)
}
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return findPreference(name, default)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
private fun <T> findPreference(name: String, default: T): T = with(prefs) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default)
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type can not be saved into Preferences")
}
res as T
}
private fun <T> putPreference(name: String, value: T) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can be saved into Preferences")
}.apply()
}
}
使用起来就简单了,所有走SharedPreferences的逻辑属性都可以通过委托PreferenceDelegate使用
private var timeMillion: String by PreferenceDelegate(this, "metaHost", "")
private var noticeState: Int by PreferenceDelegate(context, "noticeState", 0)
总结
委托可以帮助您将任务委托给其他对象,并提供更好的代码重用。Kotlin编译器创建代码以允许您无缝地使用委托。Kotlin使用简单的by关键字语法来委托属性或类。在底层,Kotlin编译器生成支持委托所需的所有代码,而不会向公共API暴露任何更改。简单地说,Kotlin生成并维护委托所需的所有样板代码,换句话说,您可以将委托委托给Kotlin。