深入kotlin - 委托

Posted 颐和园

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入kotlin - 委托相关的知识,希望对你有一定的参考价值。

委托

委托不是 java 中的概念。它仅仅是一种设计模式,但是 kotlin 从语言层级上,通过 by 关键字提供了对委托模式的支持。

类委托

interface MyInteface  // 1
	fun myPrint()

class MyInterfaceImpl(val name: String):MyInterface  //2
	override fun myPring() 
		println(name)
	

class MyClass(myInterface:MyInterface):MyInterface by myInterface // 3
fun main(args: Array<String>) 
  val myInterfaceImpl = MyInterfaceImple("zhangshan")
  MyClass(myInterfaceImpl).myPrint() // 4

  1. 接口 MyInterface 定义。
  2. 接口的实现类 MyInterfaceImpl。
  3. MyClass 委托一个 MyInterface 对象来实现 MyInterface 接口。主构造器中需要传入一个 MyInterface 对象,by 关键字后面跟委托对象名。如果 MyClass 没有具体实现,则 可以省略。
  4. 实例化 MyClass 并调用了myPrint 方法,这个方法委托了 MyInterfaceImple 对象的实现。

如果 MyClass 自己也实现了 myPrint 方法,则优先调用自己的 myPrint 方法。

属性委托

kotlin会自动创建 set/get 属性,但是你可以将这个工作委托给其它对象进行。

class MyClass 
	var name: String // 1 编译错误,属性必须赋初值或者声明为 optional

我们可以委托 name 属性给其它对象:

class MyDelegate 
	operator fun getValue(thisRef: Any?, property: KProperty<*>): String = property.name // 1
	operator fun setValue(thisRef: Any?, property:KProperty<*>, value: String) = println(value) // 2

class MyClass 
	var name: String by MyDelegate() // 3

fun main(args: Array<String>)
	val myClass = MyClass()
	myClass.name = "zhangshan" // 4 打印 zhangshan
	println(myClass.name) // 5 打印 name

  1. 属性委托的 get 方法签名必须包含 operator 关键字,方法名为 getValue,第一个参数引用的是要访问的属性所属的对象,第二个参数引用属性自身,方法返回值必须匹配要委托的属性类型。方法实现中,我们仅仅返回一个字符串,其中包含了属性自己的名字,即 name(见 MyClass 定义)
  2. 属性委托的 set 方法签名必须包含 operator 关键字,方法名为 setValue,第一、二个参数意义同 getValue,第三个参数引用要赋给属性的新值,类型必须匹配属性类型。方法实现中,我们打印了要设置的值。你实际上并不能在 setValue 中修改属性值,但你可以修改 getValue 的返回值。
  3. 将 name 属性的 get/set 方法委托给 MyDelegate 类。
  4. 调用 name 的委托 setValue 方法,这里会打印新值,即 zhangshan。
  5. 调用 name 的委托 getValue 方法,由于委托的实现是返回属性名,所以这里实际上不会打印 zhangshan,而是属性名 name。

对于只读属性,委托必须提供 getValue 方法(即ReadOnlyProperty接口),包含 2 个方法参数和一个返回值:

  • thisRef 必须是属性拥有者的类型及其父类,对于扩展属性来说,则必须是被扩展的类。
  • property 必须是 KProperty<*> 及其父类。
  • 需要返回与属性相同的类型及其子类。

对于读写属性,委托还必须多提供一个 setValue 方法(即 ReadWriteProperty 接口):

  • thisRef 与 property 参数与getValue 方法的要求一致。
  • value 的类型可以是属性的类型及其父类。

getValue 与 setValue 方法既可以作为委托类的成员方法实现,也可以作为委托类的扩展方法来实现。

注意,创建委托类时,不强制要求对 ReadOnlyProperty 和 ReadWriteProperty 接口的实现,直接实现 getValue/setValue 方法即可。

除了我们自己去从头实现一个属性委托类,Kotlin 内置的一系列现成的委托对象,它们分别用于4种特殊的情况:

  1. 延迟属性。懒加载。第一次访问时赋初值。
  2. 可观察属性,可以对属性的赋值前/后进行观察。即 swift 属性观察器。
  3. 非空属性。
  4. Map 属性。将多个属性委托给一个 Map 来操作。这样向属性赋值相当于往 Map 中动态添加属性。
延迟属性
val age: Int by lazy  // 1
	println("lazy initialize")
	return 30

fun main(arg: Array<String>)
	print(age) // 2 打印 lazy initialize
	print(age) // 3 无打印

  1. lazy 是 kotlin 提供的延迟属性的关键字,准确地说是一个 lazy() 函数,但是根据 kotlin 语法规定,如果函数最后一个参数是一个 lambda 表达式,那么可以将该表达式放到参数列表后边(即()圆括号后边)——即 swift 中所谓的尾随闭包。同时,如果函数的参数列表为空,那么()可以不写)。lazy 函数只会在该属性第一次被访问时执行,计算结果将被缓存,供后续访问。
  2. 第一次访问,打印 lazy initialize。同时 age 属性变成 30。
  3. 第二次访问,直接返回 30 而不会执行 lazy() 函数,因此没有打印。

lazy 函数有一个重载,并在参数中增加了一个线程安全的选项,用于指定线程锁的模式:

 val age: Int by lazy(LazyThreadSafetyMode.NONE)  
	println("lazy initialize")
	return 30

运行结果跟之前并无不同,因为我们并没有并行访问的情况。LazyThreadSafetyMode有 3 个值,NONE 表示没有任何线程锁,另外的两个值分别为:

  • SYNCHRONIZED: 线程同步。任何时候都只能有一个线程能初始化 lazy 属性。

  • PUBLICATION: 允许多个线程同时对 lazy 属性进行初始化,但只有第一个线程的初始值会被采用。

非空属性

非空属性是指那些在声明时就已经赋值的属性。如果我们无法在声明时确定属性值,可以将属性声明为可空属性(即 swift 的 ? 属性)这样我们每次 get 这个属性时需要在属性名后加一个?符号。但是,如果你能保证该属性一定会在第一次 get 之前赋初值,那么可以将属性声明为非空属性(即 swift 的 !属性):

class MyPerson 
	var address: String by Delegates.notNull<String>() // swift 语法:var address: String!

fun main(args: Array<String>)
  val myPerson = MyPerson()
  myPerson.address = "susan" // 注释此句,导致运行时异常
  println(myPerson.address)

Delegates 不是 class,而是一个 object。notNull 函数会标记一个指定范型的非空值。address 被标记为 notNull 之后,就可以不用在属性声明时赋初值,编译器仍然会把它识别为非空属性(而不是 optional)。你可以在后期进行赋值。换句话说,标记一个属性为非空属性,可以让 kotlin 跳过属性的非空检查,但运行时仍然需要先赋初值再访问,否则抛出异常。

可观测属性

类似 swift 的 didSet 属性观察器或者 kvo,可以监听属性值的变化。

class Person 
	var age: Int by Delegates.observable(0) // 1
		prop, oldValue, newValue -> // 2
    println("$prop.name", oldValue:$oldValue, newValue:$newValue")
	

fun main(args: Array<String>)
  val person = Person()
  person.age = 20 // 打印:age, oldValue:0, newValue: 30
  person.age = 40 // 打印:age, oldValue:20, newValue: 40 

  1. observable 函数在属性被改变之后触发。它的第一个参数是属性的初始值,第二个参数是一个 lambda 表达式。
  2. 这个 lambda 表达式接收 3 个参数,第一个参数是 KPropety 对象,代表了属性自身的 runtime 对象,第二、三个参数分别是老值和新值。

除了在 afterChange (改变后) 观察,我们也可以在beforeChange(改变前) 进行观察,这时我们可以在属性声明中使用 vetoable() 方法,并允许拒绝属性值的修改。它有两个参数,第一个参数和 observable 相同,用于指定属性的初始值,第二个参数也是一个带 3 个参数的 lambda 表达式(同 observable),但是该 lambda 表达式会在值修改之前调用。

class Person 
	var age: Int by Delegates.vetoable(20)
		prop, oldValue, newValue -> when 
			oldValue <= newValue -> true // 1
			else -> false // 2
		
	

fun main(args:Array<String>) 
	val person = Person()
	println(person.age) // print: 20
	
	person.age = 40 
	println(person.age) // print: 40
	
	person.age = 30
	println(person.age) // 3 print: 40
	

  1. 当新值大于旧值,返回 true,接收新值。
  2. 当新值小于等于旧值,返回 false,拒绝新值。
  3. 由于 30 < 40,赋值被否决,赋值不会进行,因此 age 值仍然是 40。
map 属性

用 map 存储属性,常在 JSON 解析或动态属性(类的结构不确定)中使用。这种情况下,可以将 map 实例作为类中属性的委托。

class Student(map: Map<String, Any?>)  // 1
	val name: String by map // 2
  val age: Int by map // 3
  val birthday: Date by map // 4

fun main(args: Array<String>) 
  val student = Student(mapOf( // 5
    "name" to "zhangsan",
    "age" to 30,
    " birthday" to Date()
  ))
  println(student.name)
  println(student.age)
  println(student.birthday)

  1. 主构造器参数 map 是一个 Map,key 为 String,但 value 不确定,因此用 Any?
  2. name 属性委托给 map,类型为 String
  3. age 属性委托给 map,类型为 Int
  4. birthday 属性委托给 map,类型为 Date
  5. 构造一个 map 传递给 Student 的主构造方法, kotlin 将自动初始化 Student 对象的属性值。注意 map 的 key 和 value 类型必须和 Student 的属性名和类型保持一致,否则抛出异常。

对于读写属性的 map 委托,map 对象必须是 mutable 的,这样当对属性进行赋值时,map 中对应的 value 会被赋值:

class Student(map: MutableMap<String, Any?>)  // 1
    var name: String by map // 2

fun main(args: Array<String>) 
    val map:MutableMap<String,Any?> = mutableMapOf( // 3
            "name" to "zhangsan"
    )
    val student = Student(map)
    println(student.name) // 4
    student.name = "lisi"
    println(map["name"]) // 5

  1. 主构造器参数是一个 MutableMap 而非 Map,因为 name 属性是读写属性。
  2. 将 name 属性委托给 map。
  3. 构造一个变量 map,同时显式地指定 map 的类型为 MutableMap<String, Any?>。这是必须的,否则 kotlin 类型系统会推断为 MutableMap<String, String> 类型,这样就无法和 Student 的构造参数匹配了。
  4. 打印 zhang san。
  5. 打印 lisi,因为 name 属性底层存储在 map 中,所以改变 name 的值其实就是改变 map 的值。
委托转换规则

对于每个委托属性来说,编译器会生成一个辅助属性,将对原有属性的访问委托给辅助属性。以以下属性为例

val name:String by MyDelegate()

Kotlin 会生成一个辅助属性 name$delegate,然后凡是对 name 的 get/set 访问都转发给辅助属性的 get/set 方法。

提供委托

提供委托比较少见。正常的属性委托是将属性委托给一个实现了 getValue/setValue 的对象,比如以下代码:

class Person 
	var name: String by MyDelegate()

以上代码将 name 属性委托给一个 MyDelegate 对象进行。其中 MyDelegate 实现了 getValue/setValue 方法。但是提供委托不同,它会根据一定的逻辑,决定是否要将 name 属性委托给某个委托对象。

提供委托通过 provideDelegate 方法实现。这个方法可以定义委托的创建逻辑。如果对象定义了 provideDelegate 方法,那么当创建委托对象时,这个方法就会被调用。

class People 
    val name: String by PeopleLauncher() // 1


class PeopleLauncher 
    operator fun provideDelegate(thisRef: People, property: KProperty<*>): ReadOnlyProperty<People, String>  // 2
        println("welcome")
        when (property.name)  // 3
            "name" -> return PeopleDelegate()
            else -> throw Exception("not valid name")
        
    


class PeopleDelegate: ReadOnlyProperty<People, String> 
    override fun getValue(thisRef: People, property: KProperty<*>): String  // 4
        return "zhangsan"
    



fun main(args: Array<String>) 
    val people = People()

    println(people.name) // 打印:zhangsan

  1. name 属性委托给了 PeopleLauncher 对象。
  2. PeopleLauncher 本身不是属性委托,而是提供委托——即负责提供属性委托的委托。因此它不实现 getValue/setValue 方法,它实现的是 provideDelegate 方法,这个方法需要返回一个 ReadOnlyProperty 对象——即实现了 getValue 方法的对象——属性委托对象。
  3. 在 provideDelegate 方法中,我们根据属性名进行判断,发现如果属性名是 name,则返回一个属性委托对象。如果不是,抛出异常。
  4. NameDelegate 方法是真正的属性委托,它负责实现 getValue 方法。

以上是关于深入kotlin - 委托的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 对象枚举委托

委托-kotlin

kotlin 委托

Kevin Learn Kotlin:委托

Kotlin 委托的本质以及 MMKV 的应用

Kotlin 委托的本质以及 MMKV 的应用