1. 前言
本文先从 Kotlin 属性声明, Getters 和 Setters 方法开始;重点会介绍 Backing Fields 的概念:为什么 Kotlin 中要有 field
的概念?什么时候才会用到 field
?在开发中的具体应用有哪些?之后会介绍 Backing Properties 的概念和用法。
2. 正文
2.1 属性声明
Kotlin 类的属性可以声明为可变的或者是只读的。可变属性要用到 var
关键字;只读属性需要使用到 val
关键字。
来看一下这个简单的 Student
类:
class Student(_name: String, _age: Int) {
val name: String = _name
var age: Int = _age
}
这里声明一个 name
属性,它是不可变的,只读的,用到了 val
关键字;而 age
属性,它是可变的,用到了 var
关键字。
在代码中可以这样使用:
fun main(args: Array<String>) {
val student = Student("willwaywang6",16)
// 打印学生信息
println("name=${student.name}, age=${student.age}")
}
/*
打印结果:
name=willwaywang6, age=16
*/
上面说了 name
是只读的,而 age
是可变的,我们通过代码来验证一下:
尝试将 name
重新赋值为 “willwaywang7”,代码如下:
student.name = "willwaywang7"
IDE 已经提示出:Val cannot be reassigned(val 不可以被再赋值)。这就证明了 name
确实是只读的。
将 age
重新赋值为 17,代码如下:
student.age = 17
// 再次打印学生信息
println("name=${student.name}, age=${student.age}")
/*
打印结果:
name=willwaywang6, age=17
*/
这就证明了 age
是可变的。
到这里,学过 Java 知识的同学们,都会想起 Java Bean 类的声明 Student
是下面这样的:
public class Student {
private final String name;
private int age;
public Student(String _name, int _age) {
name = _name;
age = _age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
和 Kotlin 中声明的 Student
相比:第一,Kotlin 的代码确实少了很多;第二,在使用方式上,Java 必须要通过定义的 getters 和 setters 方法来操作定义的 name
和 age
,而 Kotlin 却可以直接通过对象.name 和对象.age 来操作,这是怎么回事呢? 难道说它们都是 public
的?
带着这个疑问,我们进入下一部分:
2.2 Getters 和 Setters
通过 IDE 提供的工具:选择 Student.kt
类,通过Tools -> Kotlin -> Show Kotlin Bytecode,点击 Decompile,查看 对应的Student.decompiled.java
文件:
public final class Student {
@NotNull
private final String name;
private int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
public Student(@NotNull String _name, int _age) {
Intrinsics.checkParameterIsNotNull(_name, "_name");
super();
this.name = _name;
this.age = _age;
}
}
和 Student.java
对比一下:No difference。这下我们知道了原来 Student.kt
和 Student.java
,都是通过 getter
和 setter
方法来操作 name
和 age
的。但是我们在 Student.kt 里面确实没有声明 getter
和 setter
方法,它们是从哪里来的呢?
通过查阅官方文档Getters and Setters部分,可以知道,声明一个属性的完整语法是下面这样的:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
val <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
初始化器,getter 和 setter 都是可选的。如果属性类型可以由初始化器或者从 getter 方法返回类型推导出来的话,那么它也是可选的。在 Kotlin 中,读一个属性,例如 student.name
,就会执行 name
的 getter 访问器;改变一个属性,例如 student.age = 17
,就会执行 age
的 setter 访问器。
可以把 Student.kt
简化一下:
class Student(_name: String, _age: Int) {
val name = _name // 省略了属性类型,因为可以从初始化器里推导出来。
var age = _age
}
可以给一个属性定义自定义的访问器,可以在 Student
类中声明一个带自定义 getter
的属性 isAdult
:
class Student(_name: String, _age: Int) {
val name = _name
var age = _age
val isAdult: Boolean
get() = this.age >= 18
}
fun main(args: Array<String>) {
val student = Student("willwaywang6", 16)
// 打印学生信息
println("name=${student.name}, age=${student.age}")
println("isAdult=${student.isAdult}")
}
/*
打印结果:
name=willwaywang6, age=16
isAdult=false
*/
每次我们获取 isAdult
属性,都会进行计算,它本身并不需要字段来保存它的值。这和在类中定义一个
fun areAdult() = this.age > 18
实现的功能是一模一样的,不同的是可读性,一个是属性,另一个是方法。测试代码如下:
println("isAdult=${student.isAdult}")
println("areAdult=${student.areAdult()}")
/*
打印结果:
isAdult=false
areAdult=false
*/
好了,回归到我们讨论的主题:Student.kt
声明 name
和 age
时确实是有 getter 和 setter 方法的,只是被省略了而已。既然是这样,不省略的写法应该是怎样的呢?
按照目前的知识,不省略的写法应该是这样的:
class Student(_name: String, _age: Int) {
val name = _name
get() = this.name
var age = _age
get() = this.age
set(value) {
this.age = value
}
}
但编译器明显告诉我们,这样是不对的,看一下这张截图更清楚:
左边的提示是说,会造成 Recursive call(递归调用)。我们看一下是怎么回事:
第一步,调用 student.name;
第二步,调用 get() 方法;
第三步,在 get() 方法里,调用 this.name;
第一步,调用 student.name;
第二步,调用 get() 方法;
…
这样就形成了递归调用了 get() 方法,并且没有终止条件,所以是有问题的。
如果觉得不是很清晰,还可以看一下对应的 Java 代码:
public final class Student {
@NotNull
public final String getName() {
return this.getName();
}
public final int getAge() {
return this.getAge();
}
public final void setAge(int value) {
this.setAge(value);
}
public Student(@NotNull String _name, int _age) {
Intrinsics.checkParameterIsNotNull(_name, "_name");
super();
this.name = _name;
this.age = _age;
}
}
很明显可以看到递归调用的。
既然这样写法是不对的,那么怎么写才是正确的呢?
我们接着去看另外一条提示:Initializer is not allowed here because this property has no backing field(这里不能有初始化器,因为这个属性没有 backing field)。
这条提示给了我们很重要的信息,因为这是唯一的报错信息了。如果解决了这个报错信息,就可以编译通过了。这里面有一个新的概念:backing field。在 Java 中,可是没有见过它的。那么 backing field 到底是什么呢?接着往下看:
2.3 Backing Fields
查阅官方文档Backing Fields,先学习一下:
Kotlin 类中不能直接声明 Fields。然而,当一个属性需要一个 backing field 时,Kotlin 会自动地提供它。在访问器中使用 field
标识符就可以引用到 backing field。文档给了一个例子:
var counter = 0 // Note: the initializer assigns the backing field directly
set(value) {
if (value >= 0) field = value
}
把这个例子和我们的 Student
类的 name
和 age
属性对比一下,可以发现除了把方法体内的属性名换成了 field
之外,没有区别了。好了,我们去尝试替换一把:
class Student(_name: String, _age: Int) {
val name = _name
get() = field
var age = _age
get() = field
set(value) {
field = value
}
}
现在可以编译通过了,都正常了。
不过我们还是看一下截图,里面有一些细节的信息:
从这里看出,IDE 提示我们:现在的 getter 和 setter 是 Redundant(多余的,累赘的)。这也从另一方面说明了,现在的 getter 和 setter 正是省略掉的写法。
现在,我们一起来学习一下 backing field 的知识吧!
field
用在什么地方?
field
标识符只能用在属性的访问器中。但是却不能用在接口中具有 getter 和 setter 方法的属性,这是因为支持字段需要在接口中存储状态,而这是接口不允许的。- backing field 什么时候会生成呢?
如果一个属性用到了至少一个访问器的默认实现,就会给这个属性生成一个 backing field。这一点,在上面的例子中已经看到了。
如果一个自定义的访问通过 field 标识符引用了对应的属性,就会给这个属性生成一个 backing field。看下面的例子:
需要额外说明的是,backing field 并不是总会生成的,2.2 Getters 和 Setters 部分的class Student(_name: String, _age: Int) { val name = _name var age = _age set(value) { if (value > 0) { // 对设置的年龄,进行校验 field = value } else { throw IllegalArgumentException("age <= 0") } } } fun main(args: Array<String>) { val student = Student("willwaywang6", 16) // 打印学生信息 println("name=${student.name}, age=${student.age}") student.age = 0 println("name=${student.name}, age=${student.age}") } /* 打印信息: name=willwaywang6, age=16 Exception in thread "main" java.lang.IllegalArgumentException: age <= 0 at com.kotlin.inaction.reference.classes_and_objects.02_properties_and_fields.Student.setAge(Student.kt:12) at com.kotlin.inaction.reference.classes_and_objects.02_properties_and_fields.StudentKt.main(Student.kt:21) */
isAdult
属性就是个例子。
从有没有 backing field 的角度,可以 Kotlin 的属性分为两类:有 backing field 的属性,没有 backing field 的属性。
- 有 backing field 的属性会把值以 Java 中字段的形式存储起来。这个字段就把值存储在内存里了。
- 没有 backing field 的属性不能把值以 Java 中字段的形式存储起来。它必须每次调用时都通过计算得到它的值。
从这里可以看到,Kotlin 中的属性与 Java 中的字段相比较,是一个更高层次的概念。
2.4 Backing Properties
backing property 的出现,是为了弥补“隐含的 backing field”的不足。
backing property 可以用来实现惰性初始化,看下面的例子:
class Student(_name: String, _age: Int) {
val name = _name
var age = _age
private var _grades: Map<String, Int>? = null
val grades: Map<String, Int>
get() {
if (_grades == null) {
_grades = loadGrades()
}
return _grades!!
}
private fun loadGrades(): MutableMap<String, Int> {
println("loadGrades() called")
val result = mutableMapOf<String, Int>()
result.put("Chinese", 100)
result.put("Math", 99)
result.put("English", 100)
return result
}
}
fun main(args: Array<String>) {
val student = Student("willwaywang6", 16)
println("grades=${student.grades}")
println("grades=${student.grades}")
}
/*
打印结果:
loadGrades() called
grades={Chinese=100, Math=99, English=100}
grades={Chinese=100, Math=99, English=100}
*/
在上面的例子中,希望只有在首次访问时才加载成绩信息,并只执行一次。从打印结果来看,实现了目标。在这里,我们使用一个属性 _grades
,可空类型的,用来存储这个值,而另一个属性 grades
,非空类型的,用来提供对属性的读取访问。
backing property 可以实现数据的封装,比如我们会遇到这种需求:希望一个属性,对外表现为只读,对内表现为可读可写。看下边的例子:
class ScoreViewModel(finalScore: Int) : ViewModel() {
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
get() = _score
init {
_score.value = finalScore
Timber.i("Final score is $finalScore")
}
}
在 ScoreViewModel
里面,有一个分数的属性,希望它对外(也就是 UI Controller)表现为只读,对内(也就是 ScoreViewModel
自己)表现为可读可写。使用一个属性 _score
,private
修饰,MutableLiveData
(可变 LiveData
),用于对内的可读可写,另一个属性 score
,public
修饰,LiveData
,用于对外的只读。其中,score
就是 backing property。
需要注意的是,私有的属性是以下划线(_)开头的,而公开的属性是不以下划线开头的。这是一个代码规范。
3. 最后
从属性声明开始一步一步写到最后,并不急于抛出一个新的概念;而是自然地引出新的概念。希望这篇文章,对同学们有所帮助。
另外,对于 backing fields,backing properties 并没有进行翻译,就怕引起误解。实际上,在 《Kotlin实战》这本书里,backing fields 被翻译成“支持字段”,backing properties 被翻译成“支持属性”。这个应该比较权威吧。