深入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
- 接口 MyInterface 定义。
- 接口的实现类 MyInterfaceImpl。
- MyClass 委托一个 MyInterface 对象来实现 MyInterface 接口。主构造器中需要传入一个 MyInterface 对象,by 关键字后面跟委托对象名。如果 MyClass 没有具体实现,则
可以省略。
- 实例化 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
- 属性委托的 get 方法签名必须包含 operator 关键字,方法名为 getValue,第一个参数引用的是要访问的属性所属的对象,第二个参数引用属性自身,方法返回值必须匹配要委托的属性类型。方法实现中,我们仅仅返回一个字符串,其中包含了属性自己的名字,即 name(见 MyClass 定义)
- 属性委托的 set 方法签名必须包含 operator 关键字,方法名为 setValue,第一、二个参数意义同 getValue,第三个参数引用要赋给属性的新值,类型必须匹配属性类型。方法实现中,我们打印了要设置的值。你实际上并不能在 setValue 中修改属性值,但你可以修改 getValue 的返回值。
- 将 name 属性的 get/set 方法委托给 MyDelegate 类。
- 调用 name 的委托 setValue 方法,这里会打印新值,即 zhangshan。
- 调用 name 的委托 getValue 方法,由于委托的实现是返回属性名,所以这里实际上不会打印 zhangshan,而是属性名 name。
对于只读属性,委托必须提供 getValue 方法(即ReadOnlyProperty接口),包含 2 个方法参数和一个返回值:
- thisRef 必须是属性拥有者的类型及其父类,对于扩展属性来说,则必须是被扩展的类。
- property 必须是 KProperty<*> 及其父类。
- 需要返回与属性相同的类型及其子类。
对于读写属性,委托还必须多提供一个 setValue 方法(即 ReadWriteProperty 接口):
- thisRef 与 property 参数与getValue 方法的要求一致。
- value 的类型可以是属性的类型及其父类。
getValue 与 setValue 方法既可以作为委托类的成员方法实现,也可以作为委托类的扩展方法来实现。
注意,创建委托类时,不强制要求对 ReadOnlyProperty 和 ReadWriteProperty 接口的实现,直接实现 getValue/setValue 方法即可。
除了我们自己去从头实现一个属性委托类,Kotlin 内置的一系列现成的委托对象,它们分别用于4种特殊的情况:
- 延迟属性。懒加载。第一次访问时赋初值。
- 可观察属性,可以对属性的赋值前/后进行观察。即 swift 属性观察器。
- 非空属性。
- 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 无打印
- lazy 是 kotlin 提供的延迟属性的关键字,准确地说是一个 lazy() 函数,但是根据 kotlin 语法规定,如果函数最后一个参数是一个 lambda 表达式,那么可以将该表达式放到参数列表后边(即
()
圆括号后边)——即 swift 中所谓的尾随闭包。同时,如果函数的参数列表为空,那么()
可以不写)。lazy 函数只会在该属性第一次被访问时执行,计算结果将被缓存,供后续访问。 - 第一次访问,打印 lazy initialize。同时 age 属性变成 30。
- 第二次访问,直接返回 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
- observable 函数在属性被改变之后触发。它的第一个参数是属性的初始值,第二个参数是一个 lambda 表达式。
- 这个 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
- 当新值大于旧值,返回 true,接收新值。
- 当新值小于等于旧值,返回 false,拒绝新值。
- 由于 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)
- 主构造器参数 map 是一个 Map,key 为 String,但 value 不确定,因此用 Any?
- name 属性委托给 map,类型为 String
- age 属性委托给 map,类型为 Int
- birthday 属性委托给 map,类型为 Date
- 构造一个 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
- 主构造器参数是一个 MutableMap 而非 Map,因为 name 属性是读写属性。
- 将 name 属性委托给 map。
- 构造一个变量 map,同时显式地指定 map 的类型为 MutableMap<String, Any?>。这是必须的,否则 kotlin 类型系统会推断为 MutableMap<String, String> 类型,这样就无法和 Student 的构造参数匹配了。
- 打印 zhang san。
- 打印 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
- name 属性委托给了 PeopleLauncher 对象。
- PeopleLauncher 本身不是属性委托,而是提供委托——即负责提供属性委托的委托。因此它不实现 getValue/setValue 方法,它实现的是 provideDelegate 方法,这个方法需要返回一个 ReadOnlyProperty 对象——即实现了 getValue 方法的对象——属性委托对象。
- 在 provideDelegate 方法中,我们根据属性名进行判断,发现如果属性名是 name,则返回一个属性委托对象。如果不是,抛出异常。
- NameDelegate 方法是真正的属性委托,它负责实现 getValue 方法。
以上是关于深入kotlin - 委托的主要内容,如果未能解决你的问题,请参考以下文章