本文将伴随大家进入Kotlin语言的正式学习生涯中,希望大家不要半途而废哦!笔者将Kotlin用于Android开发中,因此将从Android开发的视角叙述相关内容,同时将与Java语言有所联系。
1. 由委托实现
1.1 简单用法
委托模式已经证明是实现继承的一个很好的替代方式, 而 Kotlin 可以零样板代码地原生支持它。 Derived
类可以通过将其所有公有成员都委托给指定对象来实现一个接口Base
:
1 | interface Base { |
Derived
的超类型列表中的 by b
子句表示 b
将会在 Derived
中内部存储, 并且编译器将生成转发给 b
的所有 Base
的方法。
1.2 覆盖由委托实现的接口成员
覆盖符合预期:编译器会使用 override
覆盖的实现而不是委托对象中的。如果将 override fun printMessage() { print("abc") }
添加到 Derived
,那么当调用 printMessage
时程序会输出abc
而不是10
:
1 | interface Base { |
但请注意,以这种方式重写的成员不会在委托对象的成员中调用 ,委托对象的成员只能访问其自身对接口成员实现:
1 | interface Base { |
2. 委托属性
有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们, 但是如果能够为大家把他们只实现一次并放入一个库会更好。例如包括:
- 延迟属性(
lazy properties
): 其值只在首次访问时计算; - 可观察属性(
observable properties
): 监听器会收到有关此属性变更的通知; - 把多个属性储存在一个映射(
map
)中,而不是每个存在单独的字段中。
为了涵盖这些(以及其他)情况,Kotlin 支持 委托属性:
1 | class Example { |
语法是: val/var <属性名>: <类型> by <表达式>
。在 by
后面的表达式是该 委托
, 因为属性对应的 get()
与 set()
会被委托给它的 getValue()
与setValue()
方法。 属性的委托不必实现任何的接口,但是需要提供一个 getValue()
函数(与 setValue()
——对于var
属性)。 例如:
1 | import kotlin.reflect.KProperty |
当我们从委托到一个 Delegate
实例的 p
读取时,将调用 Delegate
中的 getValue()
函数, 所以它第一个参数是读出 p
的对象、第二个参数保存了对 p
自身的描述 (例如你可以取它的名字)。 例如:
1 | val e = Example() |
输出结果:
1 | Example@33a17727, thank you for delegating ‘p’ to me! |
类似地,当我们给 p
赋值时,将调用 setValue()
函数。前两个参数相同,第三个参数保存将要被赋予的值:
1 | e.p = "NEW" |
输出结果:
1 | NEW has been assigned to ‘p’ in Example@33a17727. |
委托对象的要求规范可以在下文找到。
请注意,自
Kotlin 1.1
起你可以在函数或代码块中声明一个委托属性,因此它不一定是类的成员。 你可以在下文找到其示例。
2.1 标准委托
Kotlin 标准库为几种有用的委托提供了工厂方法。
2.1.1 延迟属性 Lazy
lazy()
是接受一个 lambda
并返回一个 Lazy <T>
实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get()
会执行已传递给 lazy()
的 lambda
表达式并记录结果, 后续调用 get()
只是返回记录的结果。
1 | val lazyValue: String by lazy { |
默认情况下,对于 lazy
属性的求值是同步锁的(synchronized
):该值只在一个线程中计算,并且所有线程会看到相同的值。如果初始化委托的同步锁不是必需的,这样多个线程可以同时执行,那么将 LazyThreadSafetyMode.PUBLICATION
作为参数传递给 lazy()
函数。 而如果你确定初始化将总是发生在与属性使用位于相同的线程,那么可以使用 LazyThreadSafetyMode.NONE
模式:它不会有任何线程安全的保证以及相关的开销。
2.1.2 可观察属性 Observable
Delegates.observable()
接受两个参数:初始值与修改时处理程序(handler
)。 每当我们给属性赋值时会调用该处理程序(在赋值后执行)。它有三个参数:被赋值的属性、旧值与新值:
1 | import kotlin.properties.Delegates |
如果你想截获赋值并“否决”它们,那么使用 vetoable()
取代 observable()
。 在属性被赋新值生效之前会调用传递给 vetoable
的处理程序。
2.2 把属性储存在映射中
一个常见的用例是在一个映射(map
)里存储属性的值。 这经常出现在像解析 JSON
或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。
1 | class User(val map: Map<String, Any?>) { |
在这个例子中,构造函数接受一个映射参数:
1 | val user = User(mapOf( |
委托属性会从这个映射中取值(通过字符串键——属性的名称):
1 | println(user.name) // Prints "John Doe" |
这也适用于 var
属性,如果把只读的 Map
换成 MutableMap
的话:
1 | class MutableUser(val map: MutableMap<String, Any?>) { |
2.3 局部委托属性(自 1.1 起)
你可以将局部变量声明为委托属性。 例如,你可以使一个局部变量惰性初始化:
1 | fun example(computeFoo: () -> Foo) { |
memoizedFoo
变量只会在第一次访问时计算。 如果 someCondition
失败,那么该变量根本不会计算。
2.4 属性委托要求
这里我们总结了委托对象的要求。
对于一个只读属性(即 val
声明的),委托必须提供一个名为 getValue
的函数,该函数接受以下参数:
thisRef
—— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型;property
—— 必须是类型KProperty<*>
或其超类型。
这个函数必须返回与属性相同的类型(或其子类型)。
对于一个可变属性(即 var
声明的),委托必须额外提供一个名为 setValue
的函数,该函数接受以下参数:
thisRef
—— 同getValue()
;property
—— 同getValue()
;new value
—— 必须与属性同类型或者是它的子类型。
getValue()
与 setValue()
函数可以通过委托类的成员函数提供或者由扩展函数提供。 当你需要委托属性到原本未提供的这些函数的对象时后者会更便利。 两函数都需要用 operator
关键字来进行标记。
委托类可以实现包含所需operator
方法的 ReadOnlyProperty
或 ReadWriteProperty
接口之一。 这俩接口是在 Kotlin 标准库中声明的:
1 | interface ReadOnlyProperty<in R, out T> { |
2.4.1 翻译规则
在每个委托属性的实现的背后,Kotlin 编译器都会生成辅助属性并委托给它。 例如,对于属性 prop
,生成隐藏属性 prop$delegate
,而访问器的代码只是简单地委托给这个附加属性:
1 | class C { |
Kotlin 编译器在参数中提供了关于 prop
的所有必要信息:第一个参数 this
引用到外部类 C
的实例而 this::prop
是 KProperty
类型的反射对象,该对象描述 prop
自身。
请注意,直接在代码中引用绑定的可调用引用的语法
this::prop
自Kotlin 1.1
起才可用。
2.4.2 提供委托(自 1.1 起)
通过定义 provideDelegate
操作符,可以扩展创建属性实现所委托对象的逻辑。 如果 by
右侧所使用的对象将 provideDelegate
定义为成员或扩展函数,那么会调用该函数来创建属性委托实例。
provideDelegate
的一个可能的使用场景是在创建属性时(而不仅在其 getter
或 setter 中)检测属性一致性。
例如,如果要在绑定之前检测属性名称,可以这样写:
1 | class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> { |
provideDelegate
的参数与 getValue
相同:
thisRef
—— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型;property
—— 必须是类型KProperty<*>
或其超类型。
在创建 MyUI
实例期间,为每个属性调用 provideDelegate
方法,并立即执行必要的验证。
如果没有这种拦截属性与其委托之间的绑定的能力,为了实现相同的功能, 你必须显式传递属性名,这不是很方便:
1 | // 检测属性名称而不使用“provideDelegate”功能 |
在生成的代码中,会调用 provideDelegate
方法来初始化辅助的 prop$delegate
属性。 比较对于属性声明 val prop: Type by MyDelegate()
生成的代码与上面(当 provideDelegate
方法不存在时)生成的代码:
1 | class C { |
请注意,
provideDelegate
方法只影响辅助属性的创建,并不会影响为getter
或setter
生成的代码。