第21条 使用属性代理来提取公共的属性模式

Edu

Kotlin 引入的一个支持代码重用的新特性是属性委托。它为我们提供了一种提取共有属性行为的通用方法。一个重要的例子就是 lazy 属性 —— 在第一次使用属性时才会被初始化。 这种模式非常流畅,在不支持提取属性的语言(如 Java 或者 JavaScript)中,每次都要去实现它,而在 Kotlin 中,可以通过属性委托轻松做到。在 Kotlin 标准库中,我们可以找到 lazy 函数,它返回一个实现 lazy 属性模式的属性代理:

val value by lazy { createValue() }

这并不是唯一被重复使用的属性模式。另一个重要的例子是 observable 属性 —— 当它被更改时,它就会做一些事情。例如,假设你有一个列表适配器用于绘制一个列表,每当其中的数据发生变化时,我们就需要重新绘制已更改的项。或者你可能需要记录属性的所有更改,这两种情况都可以使用 stdlib 里的 observable 实现:

var items: List<Item> by
    Delegates.observable(listOf()) { _, _, _ ->
        notifyDataSetChanged()
    }

var key: String? by
    Delegates.observable(null) { _, old, new ->
        Log.e("key changed from $old to $new") 9 
    }

从语言的角度来说,lazy 和 observable 委托属性并不是特别的,它们之所以可以被提取出来,得归功于一种更通用的属性委托机制,这种机制也可以用来提取许多其他模式。 一个很好的例子是视图和资源的绑定、依赖注入(正式的服务定位)或者数据绑定。其中许多模式需要在 Java 中使用注解来处理,但是 Kotlin 允许你使用简单、类型安全的属性委托机制来替代它们。

// View and resource binding example in Android
private val button: Button by bindView(R.id.button)
private val textSize by bindDimension(R.dimen.font_size)
private val doctor: Doctor by argExtra(DOCTOR_ARG)

// Dependency Injection using Koin
private val presenter: MainPresenter by inject()
private val repository: NetworkRepository by inject()
private val vm: MainViewModel by viewModel()

// Data binding
private val port by bindConfiguration("port")
private val token: String by preferences.bind(TOKEN_KEY)

为了理解这是如何实现的,以及如何使用委托属性提取其它常见行为,让我们从一个非常简单的属性委托开始。假设我们需要跟踪一些属性是如何使用的,为此,我们定义了自定义的 getter 和 setter 来记录它们的变化:

var token: String? = null
    get() {
        print("token returned value $field") 
        return field
    }
    set(value) {
        print("token changed from $field to $value") 
        field = value
    }

var attempts: Int = 0
    get() {
        print("attempts returned value $field")
        return field
    }
    set(value) {
        print("attempts changed from $field to $value")
        field = value
    }

尽管它们的类型不同,但这两个属性的行为几乎相同。 这似乎是一个可重复的模式,在我们项目中可能会有许多地方需要它,因此可以使用属性委托来提取此行为。 委托基于这样一种思想:属性是有它的访问器定义的 —— val 中的 getter 和 var 中的 getter 和 setter —— 这些方法可以委托给另一个对象的方法。 getter 将被委托给 getValue 函数, 而 setter 将被委托给 setValue 函数。然后我们将这样的对象放在 by 关键字的右侧,为了和上面的例子保持完全相同的属性行为,我们可以创建下面的委托:

var token: String? by LoggingProperty(null)
var attempts: Int by LoggingProperty(0) 

private class LoggingProperty<T>(var value: T) {
    
    operator fun getValue(
        thisRef: Any?,
        prop: KProperty<*>
    ): T {
        print("${prop.name} returned value $value")
        return value
    }

    operator fun setValue(
        thisRef: Any?,
        prop: KProperty<*>,
        newValue: T
    ) {
        val name = prop.name
        print("$name changed from $value to $newValue")
        value = newValue
    }
}

要充分理解属性委托的工作原理,请查看被编译为什么。上面的 token 属性将被编译成类似于下面的代码:

@JvmField
private val `token$delegate` = LoggingProperty<String?>(null) 

var token: String?
    get() = `token$delegate`.getValue(this, ::token)
    set(value) {
        `token$delegate`.setValue(this, ::token, value)
    }

正如你所见到的,getValuesetValue 方法不仅针对值进行操作, 而且它们还接收对属性的有界引用和上下文(this)。对属性的引用通常用于获取其名称,有时也获取其注解信息。 Context 为我们提供了函数被使用时的上下文信息。

当我们有多个 getValue 和 setValue 方法,但有不同的上下文类型时,在不同的情况下会选择不同的方法。这一事实可以用更聪明的方法加以利用。例如,我们可能需要一个可以在不同类型的视图中使用的委托,但对于每一个委托,它的行为都应该根据上下文提供不同的内容:

class SwipeRefreshBinderDelegate(val id: Int) {
    private var cache: SwipeRefreshLayout? = null
    
    operator fun getValue(
        activity: Activity,
        prop: KProperty<*>
    ): SwipeRefreshLayout {
        return cache ?: activity
            .findViewById<SwipeRefreshLayout>(id)
            .also { cache = it }
        }
    
    operator fun getValue(
        fragment: Fragment,
        prop: KProperty<*>
    ): SwipeRefreshLayout {
        return cache ?: fragment.view
                .findViewById<SwipeRefreshLayout>(id)
                .also { cache = it }
            }
}

为了使对象可以用作属性委托,它只需要 val 的 getValue 操作符, var 的 getValue 和 setValue 操作符。 这些操作符可以是成员函数,但也可以是扩展函数, 例如 Map<String, *> 可以用作属性委托。

val map: Map<String, Any> = mapOf(
    "name" to "Marcin", 
    "kotlinProgrammer" to true
) 

val name by map
print(name) // Marcin

这是可以的,因为在 Kotlin stdlib 中有以下扩展函数:

inline operator fun <V, V1 : V> 
    Map<in String, V>.getValue(thisRef: Any?, property: KProperty<*>): V1 = 
        getOrImplicitDefault(property.name) as V1

Kotlin 标准库中有一些我们应该知道的委托属性,例如:

  • lazy

  • Delegates.observable

  • Delegates.vetoable

  • Delegates.notNull

了解它们是有意义的,如果你注意到项目中有地方会围绕属性的一个常见模式,请记住,你可以自己创建自己的属性委托。

总结

属性委托完全控制属性, 并拥有属性关于上下文几乎所有的信息。 这个特性实际上可以用来提取仍和属性行为, lazy 、 observable 只是标准库中的两个例子。 属性委托是一个通用的提取属性模式的方法, 实践表明,有各种各样的属性模式。 它是一个强大的特性,每个 Kotlin 的开发工具中应该有它, 当我们知道这一点时,我们就有了更多的通用模式提取或定义更好的 Api 选项。

Last updated