Bootstrap

Kotlin专题「二十一」:委托和委托属性详解

前言:遇到困难时不要抱怨,既然改变不了过去,那么就努力改变未来。

一、概述

  有两个对象参与处理同一个请求,接收请求的对象将请求委托给另一个对象来处理,这就是委托。Kotlin直接支持委托模式,更优雅简洁,通过关键字 by 实现委托。

委托模式已经被证实是实现继承的一个很好替代方式,在扩展一个基类并重写方法时,基类就必须依赖子类的实现,当不断修改的时候,基类就会失去当初的性质,Kotlin 中就将类默认为 final,确保不会被修改。

有一种模式是装饰器模式,本质就是创建一个新类,实现与基类一样的接口,并且将类的实现作为一个子段保存,这样就能在基类不被修改的情况下直接修改基类的实例。但是缺点是造成很多样板代码。

    class CustomList<T>(val innerList: Collection<T> = mutableListOf<T>()) : Collection<T> {
        override val size: Int = innerList.size

        override fun contains(element: T): Boolean = innerList.contains(element)

        override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
        
        override fun isEmpty(): Boolean = innerList.isEmpty()
        
        override fun iterator(): Iterator<T> = innerList.iterator()
    }

当你实现 Collection 接口的时候就需要重写上面的方法,代码量很多,但是如果用到委托的话代码就简单很多了。

    class DelegatingCollection<T>(val innerList: Collection<T> = mutableListOf<T>()) : Collection<T> by innerList {
        override fun isEmpty(): Boolean {//重写isEmpty()方法
            return innerList.isEmpty()
        }
    }

类 DelegatingCollection 继承 Collection<T> 的所有接口,利用 by 关键字将新类 DelegatingCollection 接口的实现委托给原始类 innerList: Collection<T>,编译器会为新类自动生成接口方法,并默认返回原始类的具体实现。当然我们也可以根据自己的需要重写对应的方法。

委托模式

Kotlin 支持委托模式, 是允许对象组合实现与继承相同的代码复用,简单来说就是操作的对象不用自己去执行,而是将任务交给另一个对象去操作,这种模式叫委托模式,被操作的对象叫委托。

委托模式有两个对象参与处理同一请求,接受请求的对象将请求委托给另一个对象来处理。

二、类委托

类的委托即一个类中定义的方法实际是调用另一个类对象的方法来实现的。

以下实例中, 派生类 Derived 继承了接口 Base 的所有方法,并且委托一个传入的 Base 类的对象来执行这些方法。

    //接口
    interface Base {
        fun share()
    }

    //实现此接口的被委托的类
    class BaseIMPL : Base {
        override fun share() {
            println("BaseIMPL:实现Base接口被委托的类")
        }
    }

    //通过关键字 by 创建委托类
    class Derived(b: Base) : Base by b

    fun main(args: Array<String>) {
		val baseImpl = BaseImpl()
        Derived(baseImpl).share()
    }

打印数据如下:

BaseIMPL:实现Base接口被委托的类

Derived 类中实现 Base 接口的方法委托给另一个对象 b: Base 来处理。

在 Derived 声明中,by 字句表示:将 b 保存在 Derived 的对象实例内部,而且编译器会让 Derived 生成继承自 Base 接口的所有方法,并将调用转发给 b。

上面的例子中,我们已经委托到一个对象了,如果要修改接口里面的方法的时候,可以直接重写,而不需要重新去写新的方法。

    //通过关键字 by 创建委托类
    class Derived(b: Base) : Base by b {
        override fun share() {//重写接口的方法
            println("Derived:委托类重写Base中的方法")
        }
    }

打印数据如下:

Derived:委托类重写Base中的方法

三、属性委托

属性委托是指一个类的某个属性值不是在类中直接定义的,而是将其托付给一个代理类,从而实现对该类的属性的统一管理。有一些常见的属性类型,尽管我们可以在每次需要时手动实现它们,但最好是只实现一次,放入库中一直使用。

属性委托包括:

  • 延迟属性(lazy properties):数据只在第一次访问时计算;
  • 观察属性(observable properties):监听器会得到这个属性变化的通知;
  • Map 委托属性(Storing Properties in a Map):把多个属性值存储在一个 Map 中,而不是为每个属性存储单独的字段。

委托的语法格式:

val/var <属性名>: <类型> by <表达式>

val/var <property name>: <Type> by <expression>
  • val/var :  属性类型;
  • 属性名 :  属性名称;
  • 类型 :   属性的数据类型;
  • 表达式 :  委托代理类。

3.1 委托的底层原理

在底层,Kotlin 编译器会为每个委托属性生成一个辅助属性并委托给它。

    class Person {
        var prop: Int by Delegate()
    }

对于属性prop,会生成隐藏属性 prop$delegate,访问器的代码会简单地委托给这个附加的属性:

// 以下代码有编译器生成
class Person  {
    private val prop$delegate = Delegate()
        
    var prop: Int
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
        get() = prop$delegate.getValue(this, this::prop)
}

by 后面的表达式就是委托,属性 prop 的 get()set() 方法将委托给 delegate 对象的 getValue()setValue() 方法。

Kotlin 编译器在参数中提供了关于 prop 的所有必要信息:第一个参数 this 引用了外部类 Person 的一个实例,这个 ::prop 是 KProperty 类型的一个反射对象,该对象描述了 prop 本身。

注意:直接引用代码中绑定可调用的 this::prop 语法只在 Kotlin1.1之后才可以用。

属性委托不必实现任何接口,但必须提供一个 getValue() 函数(对于 var 属性,还需要setValue()函数)。这两个函数都需要使用 operator 关键字进行标记,意味着委托属性依赖于约定的公能,像其他约定的函数一样, getValue()setValue()可以是成员函数,也可以是扩展函数。

3.2 定义一个被委托的类

定义一个被委托的类,该类包含 getValue()setValue() 方法,且参数 thisRef 为进行委托的类的对象,property 为进行委托的属性的对象,两个函数都要使用 operator 关键字标记。

import kotlin.reflect.KProperty

//定义包含属性委托的类
class School {
    var str: String by Delegate()
}

//委托的类
class Delegate {
	//方法使用operator 修饰,thisRef 为进行委托的类的对象,property 为进行委托的属性的对象。
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, 这里委托了${property.name}属性!"
    }
	//value:表示当前属性值,必须和属性同类型或者是它的超类型。
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef 的属性${property.name}赋值为$value ")
    }
}

fun main(args: Array<String>) {
    val school = School()
    println(school.str) //访问该属性,调用了getValue()函数
    school.str = "Android" //调用 setValue() 函数
}

属性 str 委托给 Delegate 的实例时,当我们从 str 中读取(调用str的get()方法),Delegate 的 getValue() 函数会被调用,它的第一个参数 thisRef 就是我们从 str 中读取的对象,第二个参数 property 包含 str 本身的描述。

同理,当我们给 str 赋值时(调用str的set()方法),会调用 Delegate 的setValue() 函数。前两个参数是相同的,第三个保存被分配的值:

打印数据如下:

DelegatedActivity$School@758e189, 这里委托了str属性!
DelegatedActivity$School@758e189 的属性str赋值为 Android 

3.3 属性委托规则

这里我们总结了委托对象的需求:

(1)对于只读属性(val),委托必须提供具有以下参数的操作函数 getValue()

  • thisRef:   进行委托的类的对象,必须是属性所有者的相同或超类型(对于扩展属性-被扩展的类型);
  • property:  为进行委托的属性的对象,property.name 表示属性名称,必须是 KProperty<*> 类型或是其超类型。

这个函数必须返回与属性相同的类型(或其子类型)。

(2)对于可变属性(var),除了 getValue() 函数之外,它的委托必须另外提供一个名为 setValue() 的函数,带有以下参数:

  • thisRef:   进行委托的类的对象,必须是属性所有者的相同或超类型(对于扩展属性-被扩展的类型);
  • property:  为进行委托的属性的对象,property.name 表示属性名称,必须是 KProperty<*> 类型或是其超类型。
  • value:   表示当前属性值,必须和属性同类型或者是它的超类型。

注意:由于 Kotlin1.1 你可以在函数或代码块中声明委托属性,它不应该是类的成员。

四、标准委托

Kotlin 标准库为几种有用的委托提供了工厂方法,方便开发者实现这些接口来提供正确的 getValue()setValue() 方法。

4.1 标准库

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

你可以将委托创建为匿名对象,而不需要使用 Kotlin 标准库中的 ReadOnlyProperty 和 ReadWriteProperty 接口创建新类。它们提供了必需的方法:getValue() 在 ReadOnlyProperty 中声明,ReadWriteProperty 扩展了它并添加了 setValue()。因此,你可以在任何需要 ReadOnlyProperty 的时候传递 ReadWriteProperty。

    fun resourceDelegate(): ReadWriteProperty<Any?, Int> = object : ReadWriteProperty<Any?, Int> {
        var curValue = 0
        override fun getValue(thisRef: Any?, property: KProperty<*>): Int = curValue
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
            curValue = value
        }
    }

    val readOnly: Int by resourceDelegate()//ReadWriteProperty 是 val 的
    val readWrite: Int by resourceDelegate()

使用标准库中的 PropertyDelegateProvider 接口,您可以创建委托提供程序而无需要创建新类。

val provider = PropertyDelegateProvider { thisRef: Any?, property ->
    ReadOnlyProperty<Any?, Int> {_, property -> 42 }
}

val delegate: Int by provider

4.2 Lazy属性

懒加载是指在程序第一次使用到时再初始化,当再次调用时只会得到结果不会再次初始化。

lazy() 是一个函数,它接收一个 lambda 并返回一个 lazy <T> 的实例,它可以作为实现一个 lazy 属性的委托:对 get() 的第一次调用执行传递给 lazy() 的 lambda 并记住结果,对 get() 的后续调用只返回记住的结果。

	val lazyValue: String by lazy {
    	println("---lazyValue惰性初始化---")//第一次调用输出,第二次调用不执行
    	"Kotlin"
	}

	//调用
    fun main(args: Array<String>) {
        println(lazyValue)//第一次执行,执行两次输出结果
        println(lazyValue)//第二次执行,值输出返回值
    }

打印数据如下:

---lazyValue惰性初始化---
Kotlin
Kotlin

默认情况下,Lazy 属性的计算是同步的(加了同步锁synchronized):该值只在一个线程中计算,并且所有线程将看到相同的值。

如果不需要初始化委托的同步,以便多个线程可以同时执行,则通过 LazyThreadSafetyMode.PUBLICATION,作为 lazy() 函数的参数发布。

    val lazyValue2: String by lazy(LazyThreadSafetyMode.PUBLICATION) {
        println("---lazyValue惰性初始化---")
        "Kotlin"
    }

如果你初始化总是发生在你使用属性的同一个线程上,你可以使用 LazyThreadSafetyMode.NONE关闭线程安全配置 。它不会带来任何线程保证和相关开销。

    val lazyValue3: String by lazy(LazyThreadSafetyMode.NONE) {
        println("---lazyValue惰性初始化---")
        "Kotlin"
    }

4.3 被观察者observable

observable 可以用于实现观察者模式。Delegates.observable() 有两个参数:第一个是初始化值;第二个是属性值变化事件的响应器(handler)。

在执行了赋值之后都会执行属性值变化事件的响应器(handler)。它有三个参数:一个被分配的属性,旧值和新值:

    class User {
        var name: String by Delegates.observable("初始值") { 
        	property, oldValue, newValue ->
            println("observable: 旧值 == $oldValue | 新值 == $newValue")
        }
    }
    
    fun main(args: Array<String>) {
        val user = User()
        user.name = "first" //第一次赋值
        user.name = "second" //第二次赋值
    }

其中,大括号 {} 包住的代码块就是 handler 方法,打印数据如下:

observable: 旧值 == 初始值 | 新值 == first
observable: 旧值 == first | 新值 == second

如果你想拦截修改属性动作并禁止修改它们,请使用 vetoable() 取代 observable()。handler 需要返回一个 Boolean 值,true 表示同意修改,false 表示禁止修改。该回调会在修改属性值之前调用。

    class User {
        var name: String by Delegates.vetoable("初始值") { 
        	property, oldValue, newValue ->
            println("observable: 旧值 == $oldValue | 新值 == $newValue")
            return@vetoable false
        }
    }
    
    fun main(args: Array<String>) {
        val user = User()
        user.name = "first" //第一次赋值
        user.name = "second" //第二次赋值
    }

打印数据如下:

observable: 旧值 == 初始值 | 新值 == first
observable: 旧值 == 初始值 | 新值 == second

4.4 在Map中存储属性

一个常见的用法是在 Map 中存储属性值。这经常出现在解析 JSON 或做其他动态事情的应用程序中。在这种情况下,您可以使用 Map 实例本身作为委托属性的委托。

    class Student(val map: Map<String, Any?>) {
        val name: String by map
        val age: Int by map
    }

下面的例子中,构造函数有一个 map

    fun main(args: Array<String>) {
    	//构造函数接收一个Map参数
		val student: Student = Student(mapOf(
			"name" to "Kotlin",
		 	"age" to 20))
		 
		//读取Map值
		println("Student: name == ${student.name}, age == ${student.age}")
	}

委托属性从这个 Map 中获取值,打印数据如下:

Student: name == Kotlin, age == 20

如果你使用 var 的属性,需要把 Map 换成 MutableMap

    class MutlStudent(val map: MutableMap<String, Any?>) {
        val name: String by map
        val age: Int by map
    }

    fun main(args: Array<String>) {
        val map = mutableMapOf<String, Any?>(
                "name" to "Android",
                "age" to 100)
                
        val studentMutl: MutlStudent = MutlStudent(map)
        println("MutlStudent: name == ${studentMutl.name}, age == ${studentMutl.age}")

        map["name"] = "Java"
        map["age"] = 2000
        println("MutlStudent: name == ${studentMutl.name}, age == ${studentMutl.age}")
	}

打印数据如下:

MutlStudent: name == Android, age == 100
MutlStudent: name == Java, age == 2000

4.5 Not Null

Not Null 适合那些无法在初始化阶段就确定属性值的场合。

import kotlin.properties.Delegates

class Foo {
	var notNullName: String by Delegates.notNull<String>()
}

fun main(args: Array<String>) {
    Foo().notNullName = "初始化值"
    println(Foo().notNullName)
}

注意:如果属性值在赋值前就被访问,则会抛出异常。

五、本地委托属性

你可以将局部变量声明为委托属性。例如,你可以使一个局部变量惰性初始化:

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

变量 memoizedFoo 只在第一次访问时计算。如果 someCondition 失败,则根本不会计算该变量。

六、委托给另一个属性

自 Kotlin1.4 以来,一个属性可以将它的 getter 和 setter 委托给另一个属性。这种委托可用于顶级属性和类属性(成员和扩展)。delegate 属性可以是:

  • 一个顶级属性;
  • 同一个类的成员或扩展属性;
  • 另一类的成员或扩展属性。

要将一个属性委托给另一个属性,请在委托名称中使用正确的限定符 :: ,例如 this::delegate 或者 MyClass::delegate

var topLevelInt: Int = 0

class WithDelegate(val numA: Int)

class MyClass(var memberInt: Int, val instance: WithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt

    val delegatedToAnotherClass: Int by instance::numA
}

var MyClass.extDelegated: Int by ::topLevelInt

这可是非常有用的,例如,当你希望以后兼容的方式重命名属性时:引入一个新属性,用 @Deprecated 注释旧属性,并委托其实现。

class MyClass {
    var newName: Int = 0

    @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
    var oldName: Int by this::newName
}

fun main() {
    val myClass = MyClass()
    // 注意: 'oldName: Int' 已经弃用,使用'newName'替代
    myClass.oldName = 42
    println(myClass.newName) // 42
}

七、提供委托

通过定义 provideDelegate 操作符,你可以扩展创建属性实现被委托到对象的逻辑。如果 by 右侧使用的对象将 provideDelegate 定义为成员或扩展函数,则将调用该函数来创建属性委托实例。

provideDelegate 的一个最可能的用法是在创建属性(而不仅在 getter 或者 setter 中)时检查属性的一致性。例如,你想在绑定之前检查属性名,你可以这样写:

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // 创建委托
        return ResourceDelegate()
    }

    private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

class MyUI {
    fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

provideDelegate 的参数和 getValue 相同:

  • thisRef:   必须是属性所有者的相同或超类型(对于扩展性—被扩展的类型);
  • property:  必须是KProperty<*>或其超类型。

在创建 MyUI 实例期间,会为每个属性调用 provideDelegate 方法,它会立即执行必要的验证。如果没有这种拦截属性与其委托之间的绑定的能力,要实现同样的功能,你必须显式地传递属性名,这不是很方便:

// 检查属性名称而没有 "provideDelegate" 功能
class MyUI {
    val image by bindResource(ResourceID.image_id, "image")
    val text by bindResource(ResourceID.text_id, "text")
}

fun <T> MyUI.bindResource(
        id: ResourceID<T>,
        propertyName: String
): ReadOnlyProperty<MyUI, T> {
   checkProperty(this, propertyName)
   // 创建委托
}

在生成的代码中,将调用 provideDelegate 方法来初始化辅助的 prop$delegate 属性。比较对于属性声明 val prop: Type by MyDelegate() 生成的代码与上面(当 provideDelegate方法不存在时)生成的代码:

class User {
    var prop: Type by MyDelegate()
}

// 此代码由编译器生成
// 当'provideDelegate'函数可用时:
class User  {
    //调用 "provideDelegate" 创建附加的 "delegate" 属性
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
    set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

请注意,provideDelegate 方法只影响辅助属性的创建,而不影响为getter或setter生成的代码。

源码地址:https://github.com/FollowExcellence/KotlinDemo-master

点关注,不迷路


好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才

我是suming,感谢各位的支持和认可,您的点赞就是我创作的最大动力,我们下篇文章见!

如果本篇博客有任何错误,请批评指教,不胜感激 !

要想成为一个优秀的安卓开发者,这里有必须要掌握的知识架构,一步一步朝着自己的梦想前进!Keep Moving!

;