这一篇文章,让我们来谈谈关于Kotlin委托相关的知识。Kotlin中的委托可以分为类委托以及属性委托。
委托,顾名思义就是把原本需要我们实现的功能,通过委托的方式交给另一个类帮我们实现。就像工作中一样,你的同事想要喝奶茶,然后他毫不客气地让你帮他买,这样子他就把买奶茶这件事委托给了我,我来帮他完成买奶茶这件事。
在Kotlin中,我们通过关键字by来实现委托。接下来让我们先看看类的委托。
一、类委托
类的委托即一个类中定义的方法实际是调用另一个类的方法来实现的。无论什么时候实现一个接口,你都可以使用by关键字将接口的实现委托到另一个对象。下面我们自己手动来编写一个类委托实例。
1.1、首先定义Base接口
interface Base{
fun eat() //我比较喜欢吃东西,就用eat来表示喜欢吃的东西吧
}
1.2、创建Base接口的实现类
//注意:该类作为被委托的类
class BaseImpl:Base{
override fun eat() {
println("我喜欢吃垃圾食品,所以我会变胖!")
}
}
1.3、创建Base的派生类Heidou
//注意:通过关键字by关联委托
class Heidou(private val b:Base = BaseImpl()):Base by b
fun main() {
val heidou = Heidou()
heidou.eat() //输出:我喜欢吃垃圾食品,所以我会变胖!
}
我们可以看到,Heidou这个类中并不用实现eat方法,编译器会帮我们去生成它,是不是很方便呢。当然,我们也可以手动修改默认实现的方法,这个时候我们需要重写它们。
1.4、重写默认的实现方法
class Heidou(private val b:Base = BaseImpl()):Base by b{
override fun eat() {
println("现在我不吃垃圾食品了,因为我需要减肥!")
}
}
fun main() {
val heidou = Heidou()
heidou.eat() //输出:现在我不吃垃圾食品了,因为我需要减肥!
}
到这里,相信你已经掌握了类委托,接下来我们继续学习属性委托。
二、属性委托
属性委托可以让我们实现这样的功能,它处理起来比把值存储到支持字段更复杂,并且不用在每个访问器中都重复这样的逻辑,它依赖于约定。属性委托的本质就是将属性访问器的逻辑委托给一个辅助对象。
我们来看一个例子,假设,我们需要每次在设置属性值的时候,都必须打印出该属性的旧值以及最新的值。让我们先用最普通的方式进行编写。首先我们定义一个Student类,里边有姓名、科目、分数这三个属性,为了方便演示,分数的数据类型为String,代码如下:
class Student(val name:String,subject:String,score: String){
var subject:String = subject
set(newValue) {
val oldValue = field //field标识符允许你访问属性背后的支持字段
field = newValue
println("oldValue = $oldValue , newValue = $newValue")
}
var score:String = score
set(newValue) {
val oldValue = field //field标识符允许你访问属性背后的支持字段
field = newValue
println("oldValue = $oldValue , newValue = $newValue")
}
}
fun main() {
val stu = Student("张三","Java","90")
stu.subject = "Kotlin"
stu.score = "100"
}
//输出:oldValue = Java , newValue = Kotlin
// oldValue = 90 , newValue = 100
从以上代码中,在每个属性的set方法中我们都在重复着同样的工作,一两个还好,几十上百个的话维护起来就得吐血。这时候我们就可以使用属性委托来优雅的解决这个问题,我们定义一个ObservableProperty类来实现被委托的逻辑,代码如下:
class Student(val name:String,subject:String,score: String){
var subject:String by ObservableProperty(subject)
var score:String by ObservableProperty(score)
}
class ObservableProperty(var propValue: String){
/**
* 注意:
* 1、按照约定的需要,getValue和setValue函数被标记了operator
* 2、函数加了两个参数:一个用于接收属性的实例,用来设置或读取属性,另一个用于表示属性本身。这个属性类型为KProperty。
* 3、可以用过prop.name访问属性名称
*/
operator fun getValue(stu:Student,prop:KProperty<*>) = propValue
operator fun setValue(stu: Student,prop: KProperty<*>,newValue: String){
val oldValue = propValue
propValue = newValue
println("oldValue = $oldValue , newValue = $newValue")
}
}
这样子实现起来是不是很简单呢,其实在Kotlin标准库中已经为我们实现了可观察的属性逻辑,需要使用到Delegates.observable()方法,代码如下:
class Student(val name:String,subject:String,score: String){
private val observer = {
prop:KProperty<*>,oldValue:String,newValue:String ->
println("prop = ${prop.name} , oldValue = $oldValue , newValue = $newValue")
}
var subject:String by Delegates.observable(subject,observer)
var score:String by Delegates.observable(score,observer)
}
现在你大概掌握了如何自定义一个被委托的类来实现相关的访问器逻辑。属性委托的关键在于约定,我们需要约定两个方法,一个是getValue(),另一个是setValue()(可变变量使用),这两个方法都需要关键字operator来修饰。约定的这两个方法可以是成员方法,也可以是扩展函数。
三、属性委托的变换规则
通过上面的学习,现在是时候来看看属性委托的变换规则了。来看看下面的例子:
class Foo {
var p:Type by Delegate()
}
属性p将它的访问器逻辑委托给了Delegate()实例。通过关键字by把属性关联上委托对象。在上面的代码中,编译器会创建一个隐藏的辅助属性,并使用委托对象的实例进行初始化,初识属性p会委托给该实例,它被称为<delegate>
。编译器也将用一个KProperty类型的对象来代表这个属性,它被称为<property>
:
class Foo {
private val <delegate> = Delegate()
var p:Type
set(value:Type) = delegate.setValue(this,<property>,value)
get() = delegate.getValue(this,<property>)
}
按照约定,Delegate类必须具有getValue和setValue方法。接下来让我们看下使用委托实现的惰性初始化 by lazy{}
。
四、惰性初始化by lazy{}
标准库函数lazy返回的委托封装了用于存储值的支持属性和确保该值只被初始化一次的逻辑。这在当初始化过程消耗大量资源并且在使用对象时并不总是需要数据时非常有用。让我们来看一个简单的例子,创建一个整型的值,将传入的值进行返回,代码如下:
fun createValue(value:Int):Int{
println("value = $value")
return value
}
fun main() {
val value:Int by lazy { createValue(100) }
println(value)
println(value)
}
//输出:value = 100
// 100
// 100
从上面的代码可以看到,createValue方法只被调用了一次。标准库函数lazy代理了属性value的get方法,调用属性value的时候,会将lazy的值返回回来,lambda表达式在第一次调用的时候将值算出来,然后将这个值进行保存,再次调用的时候把存下来的值取出来进行返回就OK了。同理,lazy函数也定义了getValue方法。
public inline operator fun <T> kotlin.Lazy<T>.getValue(thisRef: kotlin.Any?, property: kotlin.reflect.KProperty<*>): T { /* compiled code */ }
总结:实现属性委托最关键的就是定义我们的约定:getValue方法以及setValue方法,需要关键字operator修饰。这两个方法可以是成员方法,也可以是扩展函数。通过关键字by把属性关联上委托对象。