声明属性
Kotlin中的类可以有属性,它们可以被声明成可变的var
也可以被声明成只读val
的。
class Address {
var name: String = ...
var street: String = ...
var city: String = ...
var state: String? = ...
var zip: String = ...
}
要使用属性,我们只需按名称引用它,就好像它是Java中的一个字段。
fun copyAddress(address: Address): Address {
val result = Address() // Kotlin中没有new关键词
result.name = address.name // 访问被调用
result.street = address.street
// ...
return result
}
getter和setter
声明一个属性,完整的句法是这样的:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>] //方括号内是可选的
初始值、setter、getter都是可选的。如果属性类型能通过初始值或getter返回类型推断,那么也是可选的。像下面这样:
var allByDefault: Int? // 错误: 要求显示初始值, 隐藏setter getter
var initialized = 1 //Int类型是被推算出来,默认有getter setter方法
只读属性声明的完整句法与可变属性的声明存在两点不同:val
开头替代了var
而且不允许有setter方法。
val simple: Int? //Int类型,默认有getter方法,必须在构造函数中初始化
val inferredType = 1 //Int类型,默认有getter方法
我们可以编写自定义的访问器,就像写普通函数一样,在声明属性的右边。下面举个例子:自定义getter方法:
val isEmpty: Boolean
get() = this.size == 0
自定义setter方法:
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value)
}
为了方便,setter参数名称默认使用
value
,只要你喜欢,也可以使用不同的名称。
Kotlin 版本1.1 起,如果属性类型可以从getter推断出,那么你可以省略属性类型。
val isEmpty get() = this.size == 0//Boolean 类型
如果你需要改变一个访问器的权限或添加注解,不需要改变他默认的实现。定义访问器即可,不需要定义方法体。
var setterVisibility: String = "abc"
private set // setter 是 private 而且有默认的实现
var setterWithAnnotation: Any? = null
@Inject set // setter添加Inject注解
幕后字段
Kotlin的类中不能有字段。然而当使用自定义访问器时,有时候拥有个幕后字段是很有必要的。
为此,Kotlin提供一个自动幕后字段,它可以通过标示符field
访问。
var counter = 0 // 初始值直接写给幕后字段
set(value) {
if (value >= 0) field = value
}
标识符
field
只能用在属性的访问器中。
如果一个属性至少有一个默认实现的访问器或者使用标识符field
自定义访问器时,才会生成一个幕后字段。
举个例子,像下述情况就没有幕后字段。
// 只有一个访问器且没有在自定义访问器中使用field。
val isEmpty: Boolean
get() = this.size == 0
译者注:好多人分不清属性和字段的区别。在Java和其他大多数面向对象编程语言中,字段指类中申明的变量,属性指类中对变量的读写行为(setter和getter)。而Kotlin中已经将两者合成在一起,因此若单独定义字段,将会发生冲突。
幕后属性
如果你想做的事情不符合这个“隐式的幕后字段”设计,你总可以后退一步使用幕后属性。
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // Type parameters are inferred
}
return _table ?: throw AssertionError("Set to null by another thread")
}
从各方面看,这恰好和Java一样,由于访问私有属性的默认setter和getter被优化过,所以没有引入函数调用开销。
编译时常量
在编译时已知值的属性可以用const
修饰符标记为编译常量。
这些属性需要满足以下要求:
- 在顶层或者object
的成员。
- 用String或者别的原生类型初始化。
- 没有自定义的getter。
这些属性可以用在注解中:
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }
延迟初始化属性
通常,被声明为非空类型的属性必须在构造函数中初始化。然而,这往往不是很方面。例如:属性可以通过依赖注入来初始化或者在单元测试的setup方法中初始化。在这种情况下,你不能在构造函数中提供非空初始值,但是你仍然想在类体中引用该属性时避免空检查。
为处理这种情况,你可以使用lateinit
修饰符标记属性。
public class MyTest {
lateinit var subject: TestSubject
@SetUp fun setup() {
subject = TestSubject()
} @
Test fun test() {
subject.method() // 直接引用
}
}
修饰符只能被用在类体中(不是在主构造函数中)声明的变量属性,而且属性没有自定义的setter和getter方法。属性的类型必须是非空,而且不能使原始类型。
访问没有被初始化的lateinit属性会抛出异常,该异常会详细默描述属性被访问而且事实上没有被初始化。
重写属性
参见重写属性。
委托属性
多数常见的属性都是简单的从幕后属性读取(也有可能写入)值。另一方面,使用自定义的setter和getter可以实现属性的任一行为。介于两者之间必然存在属性如何工作的常见模式。一些例子:懒值、通过key从map获取、访问数据库、访问时通知监听器,等等。
这些常见的行为可以使用委托属性实现为库。