Kotlin中的属性委托,用起来!

上篇文章我们了解了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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值