理解 Koltin 中的幕后字段和幕后属性

属性 or 字段

首先需要理解下属性和字段的区别:

字段是一个拥有值的类成员变量,可以是只读的或可变的,并可以用任何访问修饰符(例如public或private)进行标记。

属性包含一个私有字段和访问器(getter 和 setter),它也可以是只读或可变的。

var & val

Koltin 中可以通过关键字 var 和 val 来声明一个属性,完整语法如下所示:

1
2
3
4
5
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
val <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]

var 是 variable 的缩写,表示该属性是可变的,val 是 value 的缩写,表示该属性是只读的,通过代码来看下,定义一个 Koltin 类:

1
2
3
4
5
6
7
class Person {
var name = "wavever"
val age = 25
}
fun main() {
Person().age = 2333 //报错
}

通过反编译 Kotlin 字节码为 Java 文件后,可以发现,var 声明的属性会自动生存 get 和 set 方法,而 val 声明的属性则只会生成 get 方法,并且还会加上 final。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class Person {

private String name = "wavever";
private final int age = 25;

public final String getName() {
return this.name;
}

public final void setName(@NotNull String var1) {
this.name = var1;
}

public final int getAge() {
return this.age;
}
}

根据定义属性的完整语法,还可以在定义的时候添加自定义的 getter 和 setter:

1
2
3
4
5
class Person {
var name
get() = "wavever"
set(value) = print(value)
}

反编译看下 Java 代码:

1
2
3
4
5
6
7
8
9
10
public final class Person {

public final String getName() {
return "wavever";
}

public final void setName(@NotNull String value) {
System.out.print(value);
}
}

生成的 getName 和 setName 中的内容与之前定义的一致,但好像哪里不太对。。属性 name 去哪了?难道是因为声明的时候自定义了 getter 和 setter,但没有加初始值,所以给省略了么?话不多说,加上试试:

1
2
3
4
5
class Person {
var name = "wavever" // 报错
get() = "wavever"
set(value) = print(value)
}

很不幸,编译器提示有错误,看看报错信息:

Initializer is not allowed here because this property has no backing field

这个时候再反编译一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Person {

public final String getName() {
return "wavever";
}

public final void setName(@NotNull String value) {
System.out.print(value);
}

public Person() {
this.name = "wavever";
}
}

诶。。。看到属性 name 了,但还缺少声明它的地方,结合之前的报错信息,那应该就是和这个 backing field 有点关系了。

幕后字段

上边说到的 backing field 即幕后字段,结合 Kotlin 文档来看,当 getter 和 setter 有一个为默认实现,或者在 getter 和 setter 中通过 filed 标识符引用幕后字段时,才会自动生成幕后字段,怎么理解呢?再看下上边的例子,如果我们改为:

1
2
3
4
class Person {
var name = "wavever"
set(value) = print(value)
}

或是

1
2
3
4
5
class Person {
var name = "wavever"
get() = "name is $field"
set(value) = print(value)
}

可以发现都不会再报错,反编译后,可以看到也有变量 name 生成:

1
2
3
4
5
6
7
8
9
10
11
12
public final class Person {

private String name = "wavever";

public final String getName() {
return "name is " + this.name;
}

public final void setName(@NotNull String value) {
System.out.print(value);
}
}

看到这里其实就很清楚了,幕后字段在有默认访问器的情况下,需要生成访问器,访问器里必定需要使用字段,而当自定义访问器里需要使用字段值时,也必须有该字段,否则就会存在在 getter 里调用 getter 这种递归调用的情况了。

幕后属性

首先来看文档给出的实例代码以及说明:

1
2
3
4
5
6
7
8
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // 类型参数已推断出
}
return _table ?: throw AssertionError("Set to null by another thread")
}

对于 JVM 平台:通过默认 getter 和 setter 访问私有属性会被优化, 所以本例不会引入函数调用开销。

先是声明了一个私有的可变属性 _table,接着又声明了一个只读属性 stable,并在其 getter 中返回 _table。看下反编译后的 Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Map _table;

public final Map getTable() {
if (this._table == null) {
this._table = (Map)(new HashMap());
}

Map var10000 = this._table;
if (var10000 != null) {
return var10000;
} else {
throw (Throwable)(new AssertionError("Set to null by another thread"));
}
}

可以发现只有属性 _table ,而没有属性 table,这也对应了文档中的解释,在访问器中访问私有属性会被优化,因此,这里的优化就是不会去生成该属性。

因此可以看出 _table 属性对内是可变的,而对外是可读的,这就是“幕后属性”。那么它有什么作用呢?在 Koltin 中通过 private 修饰的属性是不会生成 getter 和 setter,其访问都是直接通过字段来访问,但如果此时需求是希望直接通过字段来访问,但同时又需要让外界可以获取到该属性值,那么就可以通过幕后属性来实现。

参考

Kotlin 中文文档:属性与字段

Backing properties in Kotlin

深入理解 Kotlin 类属性