[Kotlin Tutorials 16] Kotlin的属性和代理属性

Kotlin的属性和代理属性

本文收录于: https://github.com/mengdd/KotlinTutorials

基础知识

属性的声明语法:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

其中[]包含的部分都是可选的.

getter/setter如果不写就是默认实现. 类型如果不能推断出来则不能省略.

只读属性用val声明, 不允许有setter.

getter & setter

可以自定义getter和setter, 即get()set(value)方法:

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // parses the string and assigns values to other properties
    }

根据惯例, setter的参数名是value.

get()/set()比较常见的用途有: 格式化, 数据转换, 可见性封装, 只读控制, 输入验证等.

在Java中, 如果在getter/setter方法中写了一些自定义逻辑, 那么一种容易出错的情形是: 访问时不小心使用了字段本身而不是用getter/setter, 从而绕过了这些逻辑.

在Kotlin中则没有这种烦恼, 访问和修改属性都只有一种方法, 每次访问都是通过get()/set()方法.

可以添加注解或者可见性修饰, 如果仍然是默认实现可以省略方法体:

var setterVisibility: String = "abc"
    private set // the setter is private and has the default implementation

var setterWithAnnotation: Any? = null
    @Inject set // annotate the setter with Inject

Backing Fields

Kotlin中是不能直接定义fields的, 定义出来的都是property.

当property需要一个backing field时, Kotlin会自动提供. 在getter/setter中用field标识符访问.

var counter = 0 // Note: the initializer assigns the backing field directly
    set(value) {
        if (value >= 0) field = value
    }

field的存在时很有必要的. 前面说过, 访问属性一定是通过get(), 所以如果这样写:

var speed: String = "0"
    get() = "$speed km/h"

会抛出StackOverflowError, 这是因为这里发生了递归调用.

正确的写法是这样:

var speed: String = "0"
    get() = "$field km/h"

请注意getter/setter中不一定需要用到默认实现, 自定义的getter/setter中可能没有使用field标识.

val isEmpty: Boolean
    get() = this.size == 0

这种是没有backing field的.

可以定义一些属性, 其get()返回由其他属性计算得到的值.

Backing properties

如果你需要的跟这个默认的backing field不符, 你也可以用一个"backing property".

说白了就是一个private的property.

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // Type parameters are inferred
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

Overriding properties

properties可以被override, 跟方法一样, 子类可以覆盖父类.

可以用var来覆盖val, 但是反过来不行.

lateinit

不可为null的属性被声明后, 可能不能直接初始化, 在构造里也还太早.

常见的比如Android中的各种View变量, 或者是需要通过依赖注入来初始化的一些字段.

如果声明一个nullable的类型, 可以初始化为null, 但之后每次用到都要做null判断, 太不方便了.

lateinit修饰符就是用来解决这个问题的.

lateinit使用时:

  • 只能修饰var, 不能修饰val.
  • 不允许属性带声明初始化语句.
  • 不允许属性是nullable的类型.
  • 不允许属性是primitive类型.

这些要求都是显而易见的, 没写对的时候编译器都会报相应的错误提示.

如果一个property被标记为lateinit, 但是使用的时候还没有被赋值, 就会抛出异常: kotlin.UninitializedPropertyAccessException: lateinit property XXX has not been initialized.

如果想要检查是否被初始化了, 可以用.isInitialized. 注意调用的时候属性前面要加::.

但是注意文档里写:

This check is only available for the properties that are lexically accessible, i.e. declared in the same type or in one of the outer types, or at top level in the same file.

说明了它的调用条件.

举个例子, 这样是不行的:

fun main() {
    val someClass = PropertyDemo()
    if (someClass::propertyA.isInitialized) {
        println(someClass.propertyA)
    }
}

class PropertyDemo {
    lateinit var propertyA: String
}

isInitialized会被红线, 会报错:Error: Kotlin: Backing field of 'var propertyA: String' is not accessible at this point.

可以这样改:

fun main() {
    val someClass = PropertyDemo()
    if (someClass.isPropertyAInitialized()) {
        println(someClass.propertyA)
    }
}

class PropertyDemo {
    lateinit var propertyA: String
    fun isPropertyAInitialized() = ::propertyA.isInitialized
}

封装一个方法, 把.isInitialized的访问放在类内部.

Delegated Properties

属性有只管读写的默认用法, 也有自定义getter/setter的定制用法, 在这两者中间, 还有一些比较通用的套路用法:

  • lazy属性: 第一次使用的时候才初始化.
  • observable属性: 属性一经改变, 自动通知listeners.
  • 存储在map中的属性.

Kotlin用delegated properties来提供这些支持. 这样我们可以抽取出通用的部分, 供多个类共享.

代理属性语法

代理属性的语法是:

val/var <property name>: <Type> by <expression>

by之后的表达式就叫代理. 属性的get()/set()会被代理到getValue()setValue()方法.

一个例子:

class DelegateExample {
    var myProperty: String by MyDelegate()
}

class MyDelegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

从Kotlin 1.1开始, 代理属性可以在函数或者代码块内, 不是非得是类成员属性.

幸运的是, Kotlin标准库已经为常用的代理情形提供了工厂方法, 下面来看一下.

懒加载: Lazy

应用场景: 属性的计算可能比较复杂和耗时, 或者并没有一个合适的初始化时机, 于是想要在第一次使用的时候初始化, 保存属性值, 后续调用直接使用计算结果.

fun main() {
    println(lazyValue)
    println(lazyValue)
    println(lazyValue)
}

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

这个例子打印出1个"computed!"和3个"Hello".
只有第一次访问的时候会执行lambda表达式, 后续的访问都只返回结果.

注意这里默认的线程安全模式: LazyThreadSafetyMode.SYNCHRONIZED: Locks are used to ensure that only a single thread can initialize the [Lazy] instance.

可观察属性: Observable

应用场景: 观察者模式, 想要在属性发生变化的时候通知观察者.
Delegates.observable()有两个参数, 一个是初始值, 一个是onChange的lambda, 其参数包含了属性, 新旧值.

例子:

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val user = User()
    user.name = "first"
    user.name = "second"
}

输出:

<no name> -> first
first -> second

有限制的赋值: vetoable

前面我们写自定义setter的时候有一种情况是要验证输入的有效性, 过滤无效数据. 利用代理属性也可以做这件事: 用Delegates.vetoable().

Delegates.observable()类似, 也是两个参数: 初始值和onChange函数, 不同的是:

  • onChange此时返回Boolean, 只有为true的情况才会成功赋值.
  • onChange是在变化发生之前调用, 而observable()的onChange是在变化之后调用.

例子:

fun main() {
    val child = Child()

    println("try a negative age")
    child.age = -3
    println("age is: ${child.age}")
    println("try a positive age")
    child.age = 5
    println("age is: ${child.age}")
}

class Child {
    var age: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
        println("${property.name}: $oldValue -> $newValue")
        newValue > 0
    }
}

输出:

try a negative age
age: 0 -> -3
age is: 0
try a positive age
age: 0 -> 5
age is: 5

可见-3的值被拒绝了, 并没有赋值成功.

Map

应用场景举例: 有时候我们的API会设计把某些字段打包用一个map返回, 来达到一个动态个性化配置的效果.

比如定义类:

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

它的两个属性都是从map中拿的, 属性名就是key.

构造这个类时传入map即可:

val user = User(
    mapOf(
        "name" to "John Doe",
        "age" to 25
    )
)

如果是MutableMap, 那么属性是var.

自定义代理属性

如果标准库提供的这些代理属性工具都难以满足你的需求, 那么你也可以自定义.

有两个接口:ReadOnlyPropertyReadWriteProperty, 分别对应只读的属性和可读写的属性.

这两个接口只是为了方便, 并不是语法要求.

要实现自定义代理可以什么接口都不实现. 看代理属性的语法部分的例子, 只要getValue()setValue()方法签名符合即可.

思考

  • 自定义getter/setter和delegated properties有什么不同呢? -> getter/setter只局限在单个类中, 使用了代理之后, 抽取了通用的逻辑, 可以实现复用. 想象如果自己实现一个lazy的get()方法, 然后到处重复它?(lame)

参考

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

推荐阅读更多精彩内容