上篇文章我们了解了Kotlin中的接口委托,还可以使用by关键字委托属性。
使用属性委托,委托负责处理对属性的get和set函数的调用。如果您需要跨其他对象重用getter/setter逻辑,这可能非常有用,并允许您轻松扩展功能,而不仅仅是简单的支持字段。
属性委托
让我们假设你有一个Product类,它是这样定义的:
class Product(price: String, oldPrice: String)
该类的price
属性有一些格式化要求。设置price
时,要确保前缀统一添加¥符号。另外,在更新价格时,您希望自动增加更新计数属性。
你可以实现如下所示的功能:
class Product(price: String, oldPrice: String) {var price: String = priceset(value) {field = "¥$value"updateCount++}var updateCount = 0
}
在这个过程中,如果需求改变了,oldPrice原价格也需要遵循这个规则怎么办?你可以复制/粘贴这个逻辑来编写一个自定义setter,但突然你会发现自己为每个属性编写相同的setter:
class Product(price: String, oldPrice: String) {var price: String = priceset(value) {field = "¥$value"updateCount++}var oldPrice: String = oldPriceset(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 fieldstatic 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))};@NotNullprivate final PriceDelegate price$delegate;@NotNullprivate final PriceDelegate oldPrice$delegate;private int updateCount;@NotNullpublic 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 countprintln("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。