【kotlin】委托
在 kotlin 开发中,会遇到懒加载的情形:使用 by lazy
关键字。而这是通过委托来实现的。Kotlin 通过关键字 by 实现委托。
委托分为类委托和属性委托。
一、类委托
通过类委托,一个对象可以将继承自某个接口的函数全部交由另一个对象来实现。其基本形式为:
interface Interface {
fun function1(params: String)
fun function2()
}
class Impl(delegate: Interface) : Interface by delegate
class Delegate : Interface {
override fun function1(params: String) {
println("delegate function1 $params")
}
override fun function2() {
println("delegate function2")
}
}
在以上代码中,Impl
类的所有继承自 Interface
接口的函数都交由 delegate
对象来实现。
类委托的例子
类委托不仅仅只局限于单一接口。例如,假设现有一个抽象类 PhoneManufacturer
(手机生产商),定义如下:
open class PhoneManufacturer(val name: String) : ScreenManufacturer, CpuManufacturer, Assembler
interface ScreenManufacturer {
fun produceScreen()
}
interface CpuManufacturer {
fun produceCpu()
}
interface Assembler {
fun assemble()
}
其中,手机生产商 PhoneManufacturer
继承了三个接口,分别为 ScreenManufacturer
(屏幕生产商)、CpuManufacturer
(Cpu生产商)、Assembler
(组装厂)。
但是,由于手机生产商能力有限,无法将所有环节全部由自己制造,所以需要将一部分环节委托给代工厂。于是,利用委托模式,可以将 PhoneManufacturer
修改为以下形式:
open class PhoneManufacturer(
val name: String,
private val screenManufacturer: ScreenManufacturer,
private val cpuManufacturer: CpuManufacturer,
private val assembler: Assembler
) : ScreenManufacturer by screenManufacturer,
CpuManufacturer by cpuManufacturer,
Assembler by assembler {
fun sell() {
println("${name}卖出了一台由${screenManufacturer}生产屏幕、${cpuManufacturer}生产Cpu、${assembler}组装的手机")
}
}
于是,手机生产商就可以将屏幕生产、Cpu生产、组装全部外包出去,只管销售就行了。
现在,我们来测试一下效果。
fun main(args: Array<String>) {
val xiaomi = PhoneManufacturer("Xiaomi", Samsung(), Qualcomm(), Foxconn())
xiaomi.sell()
}
class Samsung : ScreenManufacturer {
override fun produceScreen() = Unit
override fun toString(): String = "Samsung"
}
class Qualcomm : CpuManufacturer {
override fun produceCpu() = Unit
override fun toString(): String = "Qualcomm"
}
class Foxconn : Assembler {
override fun assemble() = Unit
override fun toString() = "Foxconn"
}
在这里,我们新建了三个类:三星作为屏幕生产商、高通作为Cpu生产商、富士康作为组装厂;然后创建手机厂商对象 xiaomi
,并调用其 sell
函数。
输出:
Xiaomi卖出了一台由Samsung生产屏幕、Samsung生产Cpu、Foxconn组装的手机
可以看到,委托成功。也就是说,PhoneManufacturer
类虽然实现了上述三个接口,但是并没有自己实现其中的函数,而是委托给其他对象来实现了。这就是类委托。
二、属性委托
在开发中经常会遇到需要懒加载的情况:某个值在类创建的时候无法确定,需要在使用的时候进行处理。这个时候,通常可以使用 by lazy
进行数据的懒加载。
这有点类似于 lateinit var
,不过是在第一次调用的时候进行数据加载。
例如,在常规 Android 开发中,控件通常定义为 lateinit var
类型,并在 onCreate
回调之中进行 findViewById
查找控件的。
private lateinit var image: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
image = findViewById(R.id.img)
}
而使用 by lazy
进行懒加载,也是一种方式;它可以将控件的查找延后到第一次使用的时候。这样可以减少 onCreate
中要做的事,并且使代码看起来更简洁。
private val image: ImageView by lazy { findViewById(R.id.img) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_crop_bmp)
}
而懒加载利用到的就是 kotlin 中的属性委托。
如果想自己实现一个委托,很简单,只用创建一个类,并且在其中添加一个 getValue
函数返回需要的值即可。例如,我想要实现一个 findViewById
的委托,只需要添加这么一个扩展函数和委托类即可:
fun <T : View> Activity.findView(id: Int) = FindViewDelegate<T>(id)
class FindViewDelegate<T : View>(val id: Int) {
operator fun getValue(activity: Activity, kProperty: KProperty<*>): T {
return activity.findViewById(id)
}
}
其中,getValue
的第一个参数必须是属性所在类的类型,在这里是 Activity
;第二个参数是 KProperty<*>
类型,对应了需要被代理的那个属性。
然后,在任意的 Activity 中,均可以使用 findView
扩展函数进行属性委托了。
class MainActivity : AppCompatActivity() {
private val image: ImageView by findView(R.id.img)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_crop_bmp)
}
}
其他系统提供的属性委托
kotlin.properties.Delegates
工具类提供了一些系统自带的属性委托。
1. notNull
委托于 Delegates.notNull
的变量类似于 lateinit var
,只是一个类似于占位符的东西,表示这个对象不能为空,作用只是通过编译。在实际使用之前,还是需要将其进行初始化。
var name: String by Delegates.notNull()
fun main(args: Array<String>) {
val nnd = NotNullDelegate()
nnd.name = "Bob"
println(nnd.name)
}
2. observable
Delegates.observable
用于实现观察者模式,监听变量的修改。
var name: String by Delegates.observable("Bob") { property, oldValue, newValue ->
notifyNameChanged(property, oldValue, newValue)
}
fun notifyNameChanged(property: KProperty<*>, oldValue: String, newValue: String) {
// do something
}
3. vetoable
Delegates.vetoable
同样可以监听变量的修改,并且能够拦截这次修改。如果修改被拦截,则变量的值保持不变。
var name: String by Delegates.vetoable("Bob") { property, oldValue, newValue ->
// 新值不能为空且不能为"Alice",否则保持原值
newValue.isNotEmpty() && newValue != "Alice"
}