Kotlin 的 Backing Fields 和 Backing Properties

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 方法来操作定义的 nameage,而 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.ktStudent.java,都是通过 gettersetter 方法来操作 nameage 的。但是我们在 Student.kt 里面确实没有声明 gettersetter 方法,它们是从哪里来的呢?
通过查阅官方文档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 声明 nameage 时确实是有 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 类的 nameage 属性对比一下,可以发现除了把方法体内的属性名换成了 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。看下面的例子:
    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)
    */
    
    需要额外说明的是,backing field 并不是总会生成的,2.2 Getters 和 Setters 部分的 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 自己)表现为可读可写。使用一个属性 _scoreprivate 修饰,MutableLiveData(可变 LiveData),用于对内的可读可写,另一个属性 scorepublic 修饰,LiveData,用于对外的只读。其中,score 就是 backing property。
需要注意的是,私有的属性是以下划线(_)开头的,而公开的属性是不以下划线开头的。这是一个代码规范。

3. 最后

从属性声明开始一步一步写到最后,并不急于抛出一个新的概念;而是自然地引出新的概念。希望这篇文章,对同学们有所帮助。
另外,对于 backing fields,backing properties 并没有进行翻译,就怕引起误解。实际上,在 《Kotlin实战》这本书里,backing fields 被翻译成“支持字段”,backing properties 被翻译成“支持属性”。这个应该比较权威吧。

参考

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值