Effective Kotlin中文版
  • ReadMe
  • 前言
  • 第一部分:良好的代码
    • 第一章:安全性
      • 第1条:限制可变性
      • 第2条:最小化变量的作用域
      • 第3条:尽可能消除平台类型
      • 第4条: 不要暴露需要推断的类型
      • 第5条:指明你期望的参数和状态
      • 第 6 条: 优先使用标准错误,而不是自定错误
      • 第7条:当返回结果可能缺失时,优先使 null 或 Failure
      • 第8条:妥善处理空值
      • 第9条: 使用 use 来关闭资源
      • 第10条:编写单元测试
    • 第二章:可读性
      • 第11条:为了可读性设计代码
      • 第12条:操作符的行为应该与其名称一致
      • 第13条:避免返回或操作 Unit?
      • 第14条: 在变量不清晰时指定其类型
      • 第15条:考虑显式引用接收者
      • 第16:属性应该代表状态,而非行为
      • 第17条:考虑使用具名参数
      • 第18条:遵守编程惯例
  • 第二部分:良好的设计
    • 第三章:可重用性
      • 第19条:不要重复知识
      • 第20条:不要重复实现常用算法
      • 第21条 使用属性代理来提取公共的属性模式
      • 第22条:当实现公共算法时使用泛型
      • 第23条:避免隐藏类型参数
      • 第24条:在使用泛型时考虑型变
      • 第25条:在不同的平台上提取公共模块进行重用
    • 第四章:抽象设计
      • 第26条:每个方法都应该基于单一的抽象级别而编写
      • 第27条:使用抽象来保护代码不受更改
      • 第28条:指定 Api 的稳定性
      • 第29条:考虑包装扩展 API
      • 第30条:最小化元素的可见性
      • 第31条:用文档定义合约
      • 第32条:遵守抽象合约
    • 第五章:对象的创建
      • 第33条:考虑使用工厂方法代替构造函数
      • 第34条:考虑带命名默认参数的主构造函数
      • 第35条:考虑为复杂的对象创建定义 DSL
    • 第六章:类的设计
      • 第36条:组合优于继承
      • 第37条:使用数据修饰符来表示一组数据
      • 第38条:使用函数类型而不是接口来传递操作和行为
      • 第39条:类层次结构优于标签类
      • 第40条:遵守 equals 的合约
      • 第41条:遵守 hashCode 的合约
      • 第42条:遵守 compareTo 的合约
      • 第43条: 考虑将 API 的非必要部分提取到扩展函数中
      • 第44条:避免在成员中定义扩展
  • 第三部分:性能
    • 第七章:让开发成本更低
      • 第45条:避免不必要的对象创建
      • 第46条:给高阶函数使用 inline 修饰符
      • 第47条:考虑使用内联类
      • 第48条:消除过时的对象引用
    • 第八章:高效的集合处理
      • 第49条:在具有多个处理步骤的大型集合上,优先使用 Sequence
      • 第50条:限制操作步骤的数量
      • 第51条:性能关键处考虑使用原语的数组
      • 第52条:考虑使用可变集合
Powered by GitBook
On this page
  1. 第二部分:良好的设计
  2. 第三章:可重用性

第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)
    }

正如你所见到的,getValue 和 setValue 方法不仅针对值进行操作, 而且它们还接收对属性的有界引用和上下文(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 选项。

Previous第20条:不要重复实现常用算法Next第22条:当实现公共算法时使用泛型

Last updated 2 years ago