layout | title | original-doc |
---|---|---|
doc |
属性和字段 |
在Java世界中,我们已经习惯了使用getter和setter方法来访问字段。 在《Effective Java》中的第14条说明: 在公共类中,应当使用访问方法,而不是公共字段。 这样的说法已经不言而喻了。所有的IDE都支持这个:他们可以生成getters和setters,所以写出这样一个类并不困难:
{% highlight java %}
// Java public class Address {
private String name;
private String street;
private String city;
private String state;
private String zip;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
} {% endhighlight %}
这个类里的大部分代码行都是架子代码(boilerplate code)。
在Kotlin中,事实上并没有办法直接定义一个字段(fields)。你只能有属性(properties)。
可读/写的属性,使用var
关键字来定义;而只读的属性,使用val
关键字。
所以,上面的Java类在Kotlin里可以这么写:
{% highlight java %} public class Address() { // 括号定义了主构造函数 public var name : String = ... public var street String = ... public var city : String = ... public var state : String? = ... public var zip : String = ... } {% endhighlight %}
这里我们有5个可变的属性,每一个属性,编译器会生成一个相应的实际字段来存储值,以及两个访问器:getter和setter。 所以,代码生成的bytecode和上面的Java代码几乎一样。唯一的区别是属性的初始化代码,参看Null安全
(还有更好的选择,参看这里)
要使用一个属性,只需要使用名称引用即可,就相当于Java中的公共字段:
{% highlight java %} fun copyAddress(address : Address) : Address { val result = Address() // there's no 'new' keyword in Kotlin result.name = address.name // accessors are called result.street = address.street // ... return result } {% endhighlight %}
一个可变属性的完整定义语法如下:
{% highlight java %} var : [= <property_initializer>] {% endhighlight %}
上面的定义中,初始器(initializer)、getter和setter都是可选的。 属性类型(PropertyType)如果可以从初始器或者父类中推导出来,也可以省略。
示例如下:
{% highlight java %} var allByDefault : Int? // 错误:需要显式地声明初始化器,默认的getter和setter可以推导出来 var initialized = 1 // 类型是Int, 使用默认的getter和setter var setterVisiblility : String = "abc" // 需要初始化,因为不是nullable的类型 private set // setter是私有的,并且使用默认实现 {% endhighlight %}
注意公有的API(即public和protected)的属性,类型是不做推导的。 这么设计是为了防止改变初始化器时不小心改变了公有API。比如:
{% highlight java %} public val example = 1 // 错误:公有的属性必须显示指定类型 {% endhighlight %}
不可变属性的定义和可变属性有两点区别:它以val
开头,并且不允许设置setter:
{% highlight java %} val simple : Int? // 类型是Int, 默认getter, 必须在构造函数里初始化 val inferredType = 1 // 类型是Int, 默认getter {% endhighlight %}
我们可以自定义getter/setter,方式和定义普通函数很像。接着属性声明的后面,可以写函数体。例如:
{% highlight java %} val isEmpty : Boolean get() = this.size == 0 {% endhighlight %}
因为这个属性是完全由其他的属性值推导出来的,编译器不会给它单独分配一个实际字段
自定义的setter是这个样子:
{% highlight java %} var stringRepresentation : String get() = this.toString() set(value) { setDataFromString(value) // 从字符串中解析出来并向其他属性赋值 } {% endhighlight %}
我们上面已经提到,有的属性编译器会生成一个实际的字段(backing field)用来存放值。 从用户的角度看,属性是一对(有时候是一个)访问器,但实际上这些访问器可能会读取或修改一个真实的字段。 在Kotlin里,直接声明一个字段是不允许的,编译器会替我们生成他们。
在简单的情况下, 当我们没有提供自定义的访问器实现,显然属性需要有一个实际字段。否则的话,默认的访问器该做什么呢?
{% highlight java %} var counter : Int = 0 {% endhighlight %}
但是当有自定义访问器的时候,属性则不一定需要一个实际字段。
如果要访问一个属性x的实际字段,使用$x
(在Kotlin中,美元符号不能作为标识符的开头,这一点和Java不同)
{% highlight java %} var counter = 0 // 初始化的值被直接写入实际字段 set(value) { if (value >= 0) $counter = value } {% endhighlight %}
上面的$counter
字段就可以在counter属性的访问器实现里读和写。并且只能在构造函数里赋值。在其他地方,都不能使用或访问$counter
编译器会查看访问器的内部, 如果他们使用了实际字段(或者访问器使用默认实现),那么将会生成一个实际字段,否则不会生成。
例如,下面的情况下, 就没有实际字段:
{% highlight java %} val isEmpty : Boolean get() = this.size > 0 {% endhighlight %}
实际字段在这里不需要,是因为唯一的访问器并不引用它。
如果你的需求不符合这套“隐式的实际字段“方案,那么总可以使用“后背支持属性”(backing property)的方法:
{% highlight java %} 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") } {% endhighlight %}
从各种角度看,这和在Java中定义Bean属性的方式一样。因为访问私有的属性的getter和setter函数,会被编译器优化成直接反问其实际字段。
参看覆盖属性
《Effective Java 第二版》J.Bloch 第14条: 在公共类中,使用访问函数,而不是直接使用公共字段。
另外参看:JavaBeans
Java的IDE会自动生成字段的访问器
对于Java,有Project Lombok: 属性的语法是依赖于标注的。
在C#, Groovy Beans 以及Gosu语言中, 仍然声明实际字段,并显式写出getter和setter,虽然使用时和属性的访问非常像。 Scala并不区分字段和属性,但是如果想要自定义一个setter, 需要单独写一个函数,其名称依赖于规范约定。