Kotlin学习之-5.13 代理属性

Kotlin学习之-5.13 代理属性

有一些常见的属性,尽管我们可以每次需要他们的时候手动实现他们,但是最好还是可以实现一次然后随处可用,并把它放到库里。 这些例子包括:

  • 懒加载属性:属性值会在第一次访问的时候被计算
  • 可观察的属性:监听器会得到属性变化的通知
  • 存储在map中的属性,而不是单独给每个属性一个单独的变量

为了覆盖这些情况,Kotlin支持代理属性

class Example {
    var p: String by Delegate()
}

语法是:val/var <property name>: <Type> by <expression>。 在by之后的表达式就是代理,因为属性对应的get()set()方法会被代理成它的getValue()setValue()方法。属性代理不是必须实现接口,但是他们必须提供getValue()setValue()方法。 例如

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name} in $thisRef.'")
    }
}

荡我们读取p的时候,他会代理到一个Delegate对象,然后Delegate中的getValue()函数会被调用,因此对象的第一个参数是我们要读取的对象p,第二个参数持有p本身的描述。 例如:

val e = Example()
println(e.p)

这会输出

Example@33a17727, thank you for delegating 'p' to me!

类似的,当我们给p赋值的时候,setValue()方法会被调用。前两个参数和签名一样,第三个参数持有要被赋值的值:

e.p = "NEW"

这会输出

NEW has been assigned to 'p' in Example@33a17727

代理对象的具体要求在下面会详细描述。
注意从Kotlin v1.1 开始,可以在一个函数或者代码块中声明一个代理属性,它不是必须是一个类的成员。

标准代理

Kotlin标准库提供一些有用的代理工厂方法。

懒函数

lazy()函数接收一个lambda表达式并且返回一个Lazy<T>的实例,这个实例可以当做一个实现了懒属性的代理:第一次调用get()时执行传递给lazy()函数的lambda表达式并记住它的结果,后续的调用get()就直接返回记住的结果。

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)
    println(lazyValue)
}

这个例子会输出

computed!
Hello
Hello

默认情况下,懒属性的评估是同步的(synchronized):只会在一个线程中计算它的值,并且所有线程看到的值是相同的。如果不要求代理初始化的同步,那么多个线程都可以同时执行它,传递LazyThreadSafetyMode.PUBLICATION当做lazy()函数的一个参数。并且,如果你确认它的初始化总是在一个线程上执行,你可以使用LazyThreadSafeMode.NONE模式,这样就不会有任何线程安全的保障以及相应的开销。

可观察属性(Observable)

Delegates.observalbe() 接收两个参数:初始值和一个对于修改的回调。每次当我们给属性赋值的时候,赋值完成后,回调都会被调用。 它有三个参数:被赋值的属性,属性的原值,属性的新值。

import kotlin.properties.Delegates

class User {
    var name: String by Deleates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "first"
    user.name = "second"
}

这个例子会输出

<no name> -> first
first -> second

如果你想要能够拦截一个赋值并改写它,使用vetoable来替代observable()。这样传递给vetoble的回调是在赋值之前调用。

在Map中存储属性

一种常见的情况是在Map中存储属性。当解析JSON或者处理其他动态的事情时经常需要这么做,你可以使用map对象本身作为一个代理属性的代理。

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
}

在这个例子中,构造器接收一个map:

val user = User(mapOf(
    "name" to "John Dow",
    "age"  to 25
))

代理属性从map中接收值,通过属性的键值对

println(user.name) // 输出"John Doe"
println(user.age) // 输出25

对于var属性也可以使用MutableMap

class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int by map
}

局部代理属性(从Kotlin v1.1)

你可以什么局部变量作为代理属性。例如,你可以让一个局部变量延迟加载:

fun example(computeFoo: () -> Foo) {
    val memorizedFoo by lazy(computeFoo)

    if (someCondition && memorizedFoo.isValid()) {
        memorizedFoo.doSomething()
    }

}

变量memorizedFoo的值仅会在第一次被访问时计算。如果someConditon的结果是‘否’,那么变量值永远都不会被计算。

属性代理的要求

这里我们总结一下代理对象的要求。

对于一个只读属性(例如val),一个代理必须提供一个名为getValue的函数,这个函数接收下面两个参数:

  • thisRef, 必须和属性拥有者是一样的,或者是它的父类。
  • property,必须是KProperty<*>类型或者它的父类。

这个函数返回值的类型必须和属性的类型是一样的,或者是它的子类。

对于可变属性(var),一个代理必须附加提供一个名为setValue的函数,这个函数接收下面三个参数:

  • thisRefgetValue()中的一样
  • propertygetValue()中的一样
  • new value, 类型必须和属性的类型一样活着是它的父类。

getValue()setValue()函数可以被用作代理类的成员函数或者扩展函数。后者非常帮助,当你想要代理一个对象,但是它并没有提供这些方法。这两个函数都需要用关键字operator来标记。

代理类可以实现两个接口中的任意一个,ReadOnlyPropertyReadWriteProperty,他们都包含必须的operator操作符函数。 这些接口在Kotlin标准库中已经定义。

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, out t> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

翻译规则

在Kotlin中每个代理的背后是编译器生成了一个附加的属性并且代理到这个属性上。例如,对于属性prop,对应生成隐藏的属性是prop$delegate,并且访问者的代码就是肩带代理到这个附加的属性。

class C {
    var prop: Type by MyDelegate()
}

// 下面的代码是编译器生成的
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

Kotlin编译器给prop属性在参数中提供所有可能需要的信息:第一个参数this引用一个外部类C的实例,this::prop是一个KProperty类型的反射对象,可以表示prop自己。

注意this::prop这样的语法参考bound callable reference仅在Kotlin v1.1之后可用.

提供一个代理(Kotlin v1.1之后支持)

通过定义provideDelegate操作符,你可以扩展创建对象的逻辑,这些对象属性的实现是代理的。如果对象用在by的右手边,定义provideDelegate当做一个成员或者扩展函数,这个函数可以被调用来创建属性代理对象。

一个可能的provideDelegate用例是在创建属性的时候检查属性一致性,而不仅仅是在他的getter 和setter方法。

例如,如果你想要在绑定之前检查属性名,你可以这样写:

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
    }

    private fun checkProperty(thisRef: MyUI, name: String) {
    }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { }

class MyUI {
    val image by bindResource<ResourceID.image_id)
    val text by bindResource<ResourceID.text_id)
}

provideDelegate的参数是和getValue的参数是一样的:

  • thisRef, 类型必须和属性所有者的类型是一样的或者是他的父类
  • property,类型必须是KProperty<*> 或者是它的父类

provideDelegate方法在MyUI实例创建的时候会为每个属性调用,并且他会立即执行必要的验证。

如果没有在属性和他的代理之间拦截绑定的能力,想要实现同样的功能你必须显式地传递属性名,这样不是很方便。

// 不使用'provideDelegate' 功能来检查属性名,
Class MyUI {
    val image by bindResource<ResourceID.image_id, "image")
    val text by bindResource<ResourceID.text_id, "text")
}

fun <T> MyUI.bindResource(
        id: ResourceID<T>,
        propertyName: String
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(this, propertyName)
}

在生成的代码中,provideDelegate方法会被调用来初始化附加的prop$delegate属性。 比较属性声明的代码,val prop: Type by MyDelegate()

class C {
    val prop: Type by MyDelegate()
}

// 下面的代码是由编译器生成的
// 在provideDelegate 方法可用的时候

class C {
    // 调用provideDelegate 创建附加的delegate 属性
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    val prop: Type
        get() = prop$delegate.getValue(this, this::prop)
}

注意provideDelegate方法仅影响附加属性的创建并且不会影响生成代码的getter 和setter


PS,我会坚持把这个系列写完,有问题可以留言交流,也关注专栏Kotlin for Android Kotlin安卓开发

根据提供的引用内容,出现了两个关于Kotlin的错误信息。第一个引用中显示了一个无法解析依赖的错误,指出无法下载kotlin-reflect.jar文件。第二个引用中显示了一个关于kotlin-gradle-1.8.10.jar (org.jetbrains.kotlin:kotlin-reflect)",这个错误通常是由于Gradle无法找到所需的kotlin-reflect库而引起的。解决这个问题的方法是确保你的项目的Gradle配置正确,并且指定了正确的Kotlin版本。 你可以尝试以下几个步骤来解决这个问题: 1. 确保你的项目的build.gradle文件中包含了正确的Kotlin版本和kotlin-gradle-plugin版本。你可以在build.gradle文件中找到类似于以下代码的部分: ```groovy ext { kotlin_version = '1.8.10' } dependencies { // ... implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // ... } buildscript { // ... dependencies { // ... classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // ... } } ``` 请确保kotlin_version变量的值与你想要使用的Kotlin版本一致,并且在dependencies和buildscript的classpath中正确引用了kotlin-gradle-plugin。 2. 如果你已经确认了build.gradle文件中的配置正确无误,那么可能是因为Gradle无法从远程仓库下载kotlin-reflect.jar文件。你可以尝试清除Gradle的缓存并重新构建项目。在命令行中执行以下命令: ```shell ./gradlew clean ``` 然后重新构建项目: ```shell ./gradlew build ``` 这将清除Gradle的缓存并重新下载所需的依赖。 3. 如果上述步骤***切换到其他网络环境来解决这个问题。 希望以上步骤能够帮助你解决问题。如果你还有其他问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值