kotlin 学习 (14)

委托属性

有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们, 但是如果能够为大家把他们只实现一次并放入一个库会更好。例如包括:

延迟属性(lazy properties): 其值只在首次访问时计算;

可观察属性(observable properties): 监听器会收到有关此属性变更的通知;

把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。

为了涵盖这些(以及其他)情况,Kotlin 支持 委托属性:

classExample{

varp:StringbyDelegate()

}

语法是: val/var <属性名>: <类型> by <表达式>。在 by 后面的表达式是该 委托, 因为属性对应的 get()(与 set())会被委托给它的 getValue() 与 setValue() 方法。 属性的委托不必实现任何的接口,但是需要提供一个 getValue() 函数(与 setValue()——对于 var 属性)。 例如:

importkotlin.reflect.KProperty

classDelegate{

operatorfungetValue(thisRef:Any?,property:KProperty<*>):String{

return"$thisRef, thank you for delegating '${property.name}' to me!"

    }

operatorfunsetValue(thisRef:Any?,property:KProperty<*>,value:String) {

println("$value has been assigned to '${property.name}' in $thisRef.")

    }

}

当我们从委托到一个 Delegate 实例的 p 读取时,将调用 Delegate 中的 getValue() 函数, 所以它第一个参数是读出 p 的对象、第二个参数保存了对 p 自身的描述 (例如你可以取它的名字)。 例如:

vale=Example()

println(e.p)

输出结果:

Example@33a17727, thank you for delegating ‘p’ to me!

类似地,当我们给 p 赋值时,将调用 setValue() 函数。前两个参数相同,第三个参数保存将要被赋予的值:

e.p="NEW"

输出结果:

NEW has been assigned to ‘p’ in Example@33a17727.

委托对象的要求规范可以在下文找到。

请注意,自 Kotlin 1.1 起你可以在函数或代码块中声明一个委托属性,因此它不一定是类的成员。 你可以在下文找到其示例

标准委托

Kotlin 标准库为几种有用的委托提供了工厂方法。

延迟属性 Lazy

lazy() 是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

vallazyValue:Stringbylazy{

println("computed!")

"Hello"

}

funmain() {

println(lazyValue)

println(lazyValue)

}

Target platform: JVMRunning on kotlin v. 1.3.70

默认情况下,对于 lazy 属性的求值是同步锁的(synchronized):该值只在一个线程中计算,并且所有线程会看到相同的值。如果初始化委托的同步锁不是必需的,这样多个线程可以同时执行,那么将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy() 函数。 而如果你确定初始化将总是发生在与属性使用位于相同的线程, 那么可以使用 LazyThreadSafetyMode.NONE 模式:它不会有任何线程安全的保证以及相关的开销。

可观察属性 Observable

Delegates.observable() 接受两个参数:初始值与修改时处理程序(handler)。 每当我们给属性赋值时会调用该处理程序(在赋值执行)。它有三个参数:被赋值的属性、旧值与新值:

importkotlin.properties.Delegates

classUser{

varname:StringbyDelegates.observable("<no name>") {

prop,old,new->

println("$old -> $new")

    }

}

funmain() {

valuser=User()

user.name="first"

user.name="second"

}

Target platform: JVMRunning on kotlin v. 1.3.70

如果你想截获赋值并“否决”它们,那么使用 vetoable() 取代 observable()。 在属性被赋新值生效之前会调用传递给 vetoable 的处理程序。

把属性储存在映射中

一个常见的用例是在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。

classUser(valmap:Map<String,Any?>) {

valname:Stringbymap

valage:Intbymap

}

在这个例子中,构造函数接受一个映射参数:

valuser=User(mapOf(

"name"to"John Doe",

"age"to25

))

委托属性会从这个映射中取值(通过字符串键——属性的名称):

println(user.name)// Prints "John Doe"

println(user.age)// Prints 25

Target platform: JVMRunning on kotlin v. 1.3.70

这也适用于 var 属性,如果把只读的 Map 换成 MutableMap 的话:

classMutableUser(valmap:MutableMap<String,Any?>) {

varname:Stringbymap

varage:Intbymap

}

局部委托属性(自 1.1 起)

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

funexample(computeFoo: ()->Foo) {

valmemoizedFoobylazy(computeFoo)

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

memoizedFoo.doSomething()

    }

}

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

属性委托要求

这里我们总结了委托对象的要求。

对于一个只读属性(即 val 声明的),委托必须提供一个名为 getValue 的函数,该函数接受以下参数:

thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型;

property —— 必须是类型 KProperty<*> 或其超类型。

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

对于一个可变属性(即 var 声明的),委托必须额外提供一个名为 setValue 的函数,该函数接受以下参数:

thisRef —— 同 getValue();

property —— 同 getValue();

new value —— 必须与属性同类型或者是它的子类型。

getValue() 或/与 setValue() 函数可以通过委托类的成员函数提供或者由扩展函数提供。 当你需要委托属性到原本未提供的这些函数的对象时后者会更便利。 两函数都需要用 operator 关键字来进行标记。

委托类可以实现包含所需 operator 方法的 ReadOnlyProperty 或 ReadWriteProperty 接口之一。 这俩接口是在 Kotlin 标准库中声明的:

interfaceReadOnlyProperty<inR,outT>{

operatorfungetValue(thisRef:R,property:KProperty<*>):T

}

interfaceReadWriteProperty<inR,T>{

operatorfungetValue(thisRef:R,property:KProperty<*>):T

operatorfunsetValue(thisRef:R,property:KProperty<*>,value:T)

}

翻译规则

在每个委托属性的实现的背后,Kotlin 编译器都会生成辅助属性并委托给它。 例如,对于属性 prop,生成隐藏属性 prop$delegate,而访问器的代码只是简单地委托给这个附加属性:

classC{

varprop:TypebyMyDelegate()

}

// 这段是由编译器生成的相应代码:

classC{

privatevalprop$delegate=MyDelegate()

varprop:Type

get()=prop$delegate.getValue(this,this::prop)

set(value:Type)=prop$delegate.setValue(this,this::prop,value)

}

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

请注意,直接在代码中引用绑定的可调用引用的语法 this::prop 自 Kotlin 1.1 起才可用。

提供委托(自 1.1 起)

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

provideDelegate 的一个可能的使用场景是在创建属性时(而不仅在其 getter 或 setter 中)检测属性一致性。

例如,如果要在绑定之前检测属性名称,可以这样写:

classResourceDelegate<T>:ReadOnlyProperty<MyUI,T>{

overridefungetValue(thisRef:MyUI,property:KProperty<*>):T{ ... }

}


classResourceLoader<T>(id:ResourceID<T>) {

operatorfunprovideDelegate(

thisRef:MyUI,

prop:KProperty<*>

):ReadOnlyProperty<MyUI,T>{

checkProperty(thisRef,prop.name)

// 创建委托

returnResourceDelegate()

    }

privatefuncheckProperty(thisRef:MyUI,name:String) {……}

}

classMyUI{

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

valimagebybindResource(ResourceID.image_id)

valtextbybindResource(ResourceID.text_id)

}

provideDelegate 的参数与 getValue 相同:

thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型;

property —— 必须是类型 KProperty<*> 或其超类型。

在创建 MyUI 实例期间,为每个属性调用 provideDelegate 方法,并立即执行必要的验证。

如果没有这种拦截属性与其委托之间的绑定的能力,为了实现相同的功能, 你必须显式传递属性名,这不是很方便:

// 检测属性名称而不使用“provideDelegate”功能

classMyUI{

valimagebybindResource(ResourceID.image_id,"image")

valtextbybindResource(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 方法不存在时)生成的代码:

classC{

varprop:TypebyMyDelegate()

}

// 这段代码是当“provideDelegate”功能可用时

// 由编译器生成的代码:

classC{

// 调用“provideDelegate”来创建额外的“delegate”属性

privatevalprop$delegate=MyDelegate().provideDelegate(this,this::prop)

varprop:Type

get()=prop$delegate.getValue(this,this::prop)

set(value:Type)=prop$delegate.setValue(this,this::prop,value)

}

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容