[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007
系列文章:
二奶Kotlin上位记
秒懂Kotlin之彻底弄懂属性(Property)Part1
俺默认你会Java
前言
在秒懂Kotlin之彻底弄懂属性(Property)Part1中我们学习了Kotlin属性的大部分内容,相信看过的小伙伴使用的时候已经胸有成竹了。本篇来收个属性的尾,但 the last but not least ,属性的进阶知识均在本篇。例如代理属性、扩展属性、属性上如何使用注解等知识,来,让我们这些努力的程序员一起拉开对菜鸟的差距吧...
代理属性
代理模式可以说是一个很常用的设计模式了,其是一种很好的使用组合替代继承的手段。如果对它有不太清楚的地方,出门左转 秒懂Java代理与动态代理模式。
kotlin中声明一个代理属性使用by
关键字,这个关键字是非常形象的。by
左边是属性,右边是其代理,例如如下代码:读作MyDelegate()
代理了属性p
。这就意味着所有对p
的读写操作都要其代理来代为处理了。
class Demo{
var p: String by MyDelegate()
}`
那么那个代理类MyDelegate
是任何类都行吗?相信你已经猜到答案了:不行!就像你要当房地产中介(代理)一样,需要按照国家相关要求持证上岗一样,属性的代理类也要符合kotlin属性代理语法的要求。
先看一下我们的自定义代理类
class MyDelegate{
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "你好:$thisRef, 我代理 '${property.name}' 完成取值"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("你好 $thisRef , 我代理 '${property.name}' 完成赋值: $value")
}
}
通过观察,发现就两个operator
方法,getValue
代理属性的读操作,而setValue
则代理属性的写操作,是不是很简单啊?可能比较迷糊的就是这两个方法里面那些参数的含义了。
代理类要求:
- 如果代理的是只读属性(使用
val
声明),只需要一个使用operator
标识的getValue(thisRef: Any?, property: KProperty<*>)
方法 - 如果代理的是可变属性(使用
var
声明),需要getValue(thisRef: Any?, property: KProperty<*>)
和setValue(thisRef: Any?, property: KProperty<*>, value: String)
两个方法
方法参数的含义如下:
- thisRef: Any?
用来调用属性的那个类实例,例如此处的Demo
。 - property: KProperty<*>
属性的元数据,例如此处是p
的元数据,使用反射可以获取这些数据。 - value: String
要为属性设置的值,例如此处的"ShuSheng007"。其类型要与我们声明的属性一致,此处我们属性p的类型是String,所以此value的类型也是String。
第三个参数还有点用,其他两个参数我们不用管,基本没什么jb用。
上面定义好了属性代理,那让我们实际使用一下这个代理属性吧,其与普通属性使用方式完全一致。
Demo().run {
println("赋值前:$p")
p="ShuSheng007"
println("赋值后:$p")
}
输出:
赋值前:你好:top.ss007.learn.kotlin.properties.Demo@76fb509a, 我代理 'p' 完成取值
你好 top.ss007.learn.kotlin.properties.Demo@76fb509a , 我代理 'p' 完成赋值: ShuSheng007
赋值后:你好:top.ss007.learn.kotlin.properties.Demo@76fb509a, 我代理 'p' 完成取值
可见,当读取属性p
的值时,代理类的getValue执行了;当为属性p
设置值时,代理类的setValue执行了 。
至此,有些聪明的同学可能已经发现了上面的代码的问题:给p
设置新值 "ShuSheng007",但p的值却没有变化,你这不是在搞笑吗?这不是白设置了吗?我只能惊叹一声:你为什么如此睿智?
我最开始的时候试图从setValue
和getValue
来想办法,但是我失败了,如果哪位同学对此有比较深刻的理解麻烦评论区告知。官方文档中是使用一个我们自定义的字段来完成这个功能。
class MyDelegate {
private var myValue: String = "who"
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "你好:$thisRef, 我代理 '${property.name}' 完成取值:$myValue"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
myValue = value
println("你好 $thisRef , 我代理 '${property.name}' 完成赋值: $value")
}
}
如上代码所示,我们在代理类中使用了一个myValue
私有back field。
属性代理接口
通过上面的讲解我们知道了如何定义代理类,但是我们需要记住那两个方法的签名,写起来还是有一定的困。于是Kotlin 就很贴心的为我们提供了两个接口ReadOnlyProperty
与ReadWriteProperty
。顾名思义,一个用于只读代理属性,另一个用于读写代理属性。
//用于只读代理属性
public fun interface ReadOnlyProperty<in T, out V> {
public operator fun getValue(thisRef: T, property: KProperty<*>): V
}
//用于读写代理属性
public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
public override operator fun getValue(thisRef: T, property: KProperty<*>): V
public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}
其中泛型参数T
为声明属性时的那个类的类型,V
为属性的类型。
例如对于我们的自定义代理属性p
来说:T
为Demo,V
为String,如下代码所示:
class Demo{
var p: String by MyDelegate()
}`
让我们使用ReadWriteProperty来改造一下我们的MyDelegate
类
class MyDelegate :ReadWriteProperty<Demo,String> {
override fun getValue(thisRef: Demo, property: KProperty<*>): String {
...
return ""
}
override fun setValue(thisRef: Demo, property: KProperty<*>, value: String) {
...
}
}
糖衣下的酮体
明白了如何使用,让们来看下其糖衣下面的真身白不白吧,下面是反编译后的Java代码。
首先我们的代理类MyDelegate
几乎没有变化,关键内幕都在声明类Demo
中
public final class Demo {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Demo.class, "p", "getP()Ljava/lang/String;", 0))};
@NotNull
private final MyDelegate p$delegate = new MyDelegate();
@NotNull
public final String getP() {
return this.p$delegate.getValue(this, $$delegatedProperties[0]);
}
public final void setP(@NotNull String var1) {
this.p$delegate.setValue(this, $$delegatedProperties[0], var1);
}
}
- 为属性
p
生成getter
和setter
方法。 - 生成并持有一个代理类的对象实例
p$delegate
private final MyDelegate p$delegate = new MyDelegate();
- 在
getter
和setter
方法中通过p$delegate
来调用其getValue
和setValue
方法
不得不说,真白!真的太直白了...
标准代理属性
至此,代理属性就已经很清楚了,我们可以通过此机制依据自己的想象力自由发挥了。但是一些代理属性的逻辑我们经常会用到,所以官方很贴心的为我们封装在了标准库当中。
Lazy
使用Layz<T>
代理的属性,只有第一次取值时候才会通过执行传入的Lambda表达式获得结果。以后多次调用时会直接返回第一次的值,而不是再执行一次Lambda表达式。
val lazyValue: String by lazy {
println("我们大家一起喊:")
"我们都爱 ShuSheng007"
}
执行
println("第1次:$lazyValue")
println("第2次:$lazyValue")
结果:
我们大家一起喊:
第1次:我们都爱 ShuSheng007
第2次:我们都爱 ShuSheng007
我们连续调用了两次lazyValue属性的值,但只输出了一次 我们大家一起喊:,可见lambda只在第一次取值时执行了。
注意:lazy中lambda的返回值的类型就是属性的值的类型。
Observable
在属性值改变后会收到通知。
var name: String by Delegates.observable("ben") {
prop, old, new ->
println("属性${prop.name}的值从: $old -> $new")
}
调用
name="shusheng007"
println("My name is:$name")
结果:
属性name的值从: ben -> shusheng007
My name is:shusheng007
可见,当我们修改name的值时,observable传入的lambda表达式被执行了。
Map作为代理
Map<String,Any?>
或者MutableMap<String,Any?>
可以作为以其key
命名的属性的代理。 然后那个属性就和map里的对应key-value
绑定在一起了。
val map = mapOf<String, Any?>(
"netName" to "ss007",
"age" to 35
)
val netName: String by map
val age: Int by map
调用:
println("my netName is $netName and I am $age years old")
输出:
my netName is ss007 and I am 35 years old
可见,读取属性的值时是从代理map
中获得的。
注意:属性的名称必须与map中对应的key保持一致,不然会报错。
如果代理是MutableMap<String,Any?>
,则可以代理var
属性,对属性值的改变会反映到map中,反之亦然。
代理提供者(provider)
这部分内容不感兴趣的完全可以略过了,我自己也没用过,所以也不会深入讲解。潜台词:俺其实也不太会,如果你看不懂不要问俺,不要问俺...
kotlin1.4 提供了一个操作符provideDelegate
, 通过它我们可以创建一个属性delegate的provider,然后在那个代理实例返回前做一些校验工作。
如下代码所示,我们写了一个MyDelegateProvider
类用来对外提供代理MyDelegate
实例
class MyDelegate : ReadWriteProperty<Demo, String> {
override fun getValue(thisRef: Demo, property: KProperty<*>): String {
return "ss007坐怀不乱"
}
override fun setValue(thisRef: Demo, property: KProperty<*>, value: String) {
}
}
class MyDelegateProvider{
operator fun provideDelegate(thisRef: Demo, property: KProperty<*>):ReadWriteProperty<Demo,String>{
//在返回属性代理之前可做一些校验工作
println("做了一些扫黄打非的工作")
return MyDelegate()
}
}
我们可以使用此代理提供者来代理属性了,如下所示
class Demo {
var p: String by MyDelegateProvider()
下面为读取属性p
时的输出,可见我们可以通过MyDelegateProvider
类中的provideDelegate()
方法中控制代理类实例对象的生成了。
做了一些扫黄打非的工作
ss007坐怀不乱
扩展属性
这个语法糖不得不说真甜,我很喜欢。通过扩展给人的感觉就像我们可以随意给别人写的类添加方法和属性,以前要扩展别人写的类可能就要使用到继承了,如果那个类是final
的,即不允许继承就抓瞎了。
假设现在我需要获得一个List
最后一个item
的index,那么在Java中如何实现呢?有点经验的程序员很容易就会写出如下Util方法。
public final class Util{
...
public static final <T>int lastIndex(@NotNull List<T> list){
Objects.requireNonNull("list 不能为null");
return list.size()-1;
}
}
调用方式如下:
//java
List<String> list= new ArrayList<String>();
int lastIndex= Util.lastIndex(list);
看起来也还不错啦,但是如果List
本身就包含一个叫lastIndex
属性的话,我们就不用使用Util方法了,而且使用起来更加自然,更加符合自然语义,如下所示
//java
int lastIndex = list.lastIndex;
Kotlin 也是这样想的!使用Kotlin扩展属性为List
添加(扩展)一个名为lastIndex
只读属性后我们就可以按照上面的方式获取一个list的lastIndex了。
//kotlin
public val <T> List<T>.lastIndex: Int
get() {
return this.size - 1
}
扩展属性的语法非常简单:在普通属性名称前面加上.要扩展的类型
,注意前面那个点。具体的扩展逻辑写在getter
与setter
方法里。
扩展属性一般为顶层属性,即直接写在包名下,不使用类包裹。写在类中的情形很少,也比较复杂,等到专门写扩展方法的时候再说吧。
值得注意的是,扩展属性不可以有初始化器,即不可以赋初始值,下面的写法会报错
//kotlin
public val <T> List<T>.lastIndex: Int = 0
get() {
return this.size - 1
}
这个也很好理解,因为我们不能真的给那个要扩展的类加入一个成员字段。例如我们为一个自定义类Student
扩展了一个属性alias
,虽然用的时候alias
就好像是Student的属性一样,但是我们用屁股想也明白,这只是一件糖衣,我们怎么可能随意更改三方代码呢?
var Student.alias: String
get() {
return "$name 有个网名叫ss007"
}
set(value) {
name = value
println("后来其直接将名字也改成了$value")
}
上面的代码反编译为Java后只是对应了两个Util方法,一个getter,一个setter。因为无法生成back field,所以不可以有初始化器,而且setter方法set时也只能set扩展类里面的其他public
的可变属性。
public final class ExtensionsKt {
@NotNull
public static final String getAlias(@NotNull Student $this$alias) {
//省略校验null代码
return $this$alias.getName() + " 有个网名叫ss007";
}
public static final void setAlias(@NotNull Student $this$alias, @NotNull String value) {
//省略校验null代码
$this$alias.setName(value);
String var2 = "后来其直接将名字也改成了" + value;
System.out.println(var2);
}
}
你看,和我们最开始手动写的那个Util方法一毛一样。我们写一个扩展属性或者方法,就相当于写了一个Util方法,但就是因为换了个更人性化的调用方式,瞬间感觉高大上了有没有。
这就好比kotlin与Java争夺程序员老爷欢心的时候:
kotlin:老爷你闭上眼睛,我给你表演个魔术。只见她拿钢丝将自己悬于半空(写了个扩展属性或者方法),老爷兴奋极了。
Java:我可以,然后拿了根很粗的绳子把自己吊了起来(写了个Util方法),老爷回头一看差点没气死...
程序员老爷:你当我瞎啊,那么粗的绳子我看不见吗?
然后他觉还是kotlin厉害,但老爷不想在客厅了,要到卧室去玩这个游戏。Java说自己办不到,因为卧室没有梁,无法吊起来。老爷殷切的望向了kotlin,kotlin突然哽咽了:卧室没有梁(无法生成back field,所以无法使用初始化器)臣妾也做不到啊!
注解属性
这块涉及到了注解的知识,简单聊一下如何给属性添加注解
首先我们定义一个注解
@Target(AnnotationTarget.FIELD,
AnnotationTarget.PROPERTY_SETTER,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY
)
@Retention(AnnotationRetention.RUNTIME)
annotation class Focus
与Java的语法差异比较大,kotlin是直接在一个class前面加上annotaion
关键字来声明注解。
那个元注解@Target
非常重要,它决定了Focus
注解可以使用的范围。
首先我们定义一个Car类,里面有三个属性,其中一个是在构造函数中声明的只读属性
class Car(var wheel: Int) {
var speed: Int = 0
set(value) {
field = value
}
get() {
return 100
}
val weight: Int
get() = 1000
}
其反编译后的Java代码如下:
public final class Car {
private int speed;
private final int wheel;//构造函数中声明的属性会生成back field
public Car(int wheel) {
this.wheel = wheel;
}
public final int getSpeed() {
return 100;
}
public final void setSpeed(int value) {
this.speed = value;
}
public final int getWheel() {
return this.wheel;
}
public final void setWheel(int var1) {
this.wheel = var1;
}
//只读属性weight没有生成back field
public final int getWeight() {
return 1000;
}
}
一定要先好好看看对应的Java代码,然后我们开始给这些属性添加注解。
Kotlin注解相比Java注解来说较为复杂,主要是因为在Kotlin中一个代码元素编译器会生成多个相应的代码元素,用户需要负责指出他想对哪个生成的代码元素进行注解,你说这不是扯呢吗?有的用户连kotlin会生成什么样的代码元素都不知道,你让他怎么决定!
kotlin提供了一些称作Use-site Targets,意思就是给你几个表示注解目标的关键字,让你在使用的时候自己决定用哪个。
这里列出属性相关的:
- property (annotations with this target are not visible to Java);
- field;
- get (property getter);
- set (property setter);
普通声明的属性
这些属性相对来说比较简单,像speed
和weight
。
class Car(var wheel: Int) {
@field:Focus var speed: Int = 0
@Focus set(value) {
field = value
}
@Focus get() {
return 100
}
val weight: Int
@Focus get() = 1000
}
如上代码所示,要为getter或者setter方法加注解,那么写在对应的方法前面就好了,不需要使用Use-site Targets。要为field添加注解的话,就要使用@field
了,生成代码如下:
@Focus
private int speed;
...
构造函数中的属性
此类属性就很依赖Use-site Targets了,因为其没有办法声明getter和setter方法。
class Car(@set:Focus var wheel: Int) {
}
生成代码如下:
@Focus
public final void setWheel(int var1) {
this.wheel = var1;
}
其他几个同理。
注意:最为费解的就是@property
了,如果我们的自定义注解Focous
支持Property
类型,那么写在属性声明前的注解默认都是这个类型,而这个类型对于Java 是不可见的。
例如我们直接给构造函数中的属性添加@Focus
或者@property:Focus
class Car(@Focus var wheel: Int) {
...
}
反编译后Java代码如下,可见这个方法是编译器生成的,是属于kotlin特有的特性吧
/** @deprecated */
// $FF: synthetic method
@Focus
public static void getWheel$annotations() {
}
总结
行文中有些地方会有些许模糊,那就可能是我还没有深入的使用过那块内容,自己的理解可能还不到位。如有不明白的地方,欢迎大家一起讨论,一起进步...
猿猿们又到了点赞分享的时候了,有钱的捧个钱场(打赏),没钱的就捧个人场(点赞/分享)... 猿猿们的美德在哪里?
AD: 认准公众号《华北01学会》,带你装逼带你飞!——ShuSheng007
文章评论