属性声明
属性可以使用 var
关键字声明为可变的,也可以使用 val
关键字声明为只读的。
kotlin
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
var state: String? = null
var zip: String = "123456"
}
要使用这些属性,只需通过属性名来引用它们。
kotlin
fun copyAddress(address: Address): Address {
val result = Address()
result.name = address.name
result.street = address.street
return result
}
getter
和 setter
属性声明的完整语法如下:
kotlin
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
这里的初始值设定项、getter
和 setter
都是可选的。如果属性的类型可以从初始值设定项或者 getter
的返回类型推导出来,那么属性类型也是可选的。
只读属性声明的完整语法和可变属性声明有两点不同:
-
以
val
开头而非var
。 -
不允许有
setter
。
kotlin
// 类型为 Int,有默认的 getter,必须在构造函数中初始化。
val simple: Int?
// 类型为 Int,有默认的 getter。
val inferredType = 1
你可以为属性定义自定义的访问器。要是定义了自定义的 getter
,每次访问该属性时都会调用它(这样就能实现一个计算属性)。下面是一个自定义 getter
的示例:
kotlin
class Rectangle(val width: Int, val height: Int) {
// 属性类型可选,因为能从 getter 的返回类型推导出来。
val area: Int
get() = this.width * this.height
}
如果属性类型可以从获取器 getter
中推断出来,那么你可以省略该属性的类型声明。
kotlin
class Rectangle(val width: Int, val height: Int) {
val area get() = this.width * this.height
}
当你定义了一个自定义的 setter
方法后,除了属性初始化的时候,每次给该属性赋值都会调用这个自定义 setter
。
kotlin
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value)
}
按照惯例,setter
参数的名称是 value
,但如果你愿意,也可以选择其他名称。
如果你需要为访问器添加注解或更改其可见性,但又不想改变默认实现,那么可以只定义访问器而不定义其主体:
kotlin
var setterVisibility: String = "abc"
private set // 该 setter 是私有的,且采用默认实现。
kotlin
var setterWithAnnotation: Any? = null
@Inject set // 为该 setter 添加 Inject 注解。
幕后字段
字段仅作为属性的一部分,用于在内存中存储属性的值。字段不能直接声明。
不过,当属性需要一个幕后字段时,Kotlin
会自动提供。
在访问器中,可以使用 field
标识符来引用这个幕后字段:
kotlin
var counter = 0 // 初始化器会直接为幕后字段赋值。
set(value) {
if (value >= 0)
field = value
// 错误,会导致栈溢出:使用实际名称 counter 会使 setter 递归调用。
// counter = value
}
field
标识符只能在属性的访问器中使用。
如果一个属性至少有一个访问器使用默认实现,或者自定义访问器通过 field
标识符引用它,那么就会为该属性生成一个幕后字段。
例如,在以下情况下就不会生成幕后字段:
kotlin
val isEmpty: Boolean
get() = this.size == 0
后备属性
如果你想实现一些无法通过隐式幕后字段机制完成的操作,那么你总可以采用后备属性的方式:
kotlin
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
的私有属性,其访问操作会被优化,以避免函数调用带来的开销。
编译时常量
如果一个只读属性的值在编译时就已知,那么可以使用 const
修饰符将其标记为编译时常量。这样的属性需要满足以下要求:
-
它必须是顶级属性,或者是
object
声明或伴生对象的成员。 -
它必须用
String
类型或基本数据类型的值进行初始化。 -
它不能有自定义的
getter
。
编译器会对常量的使用进行内联处理,将对常量的引用替换为其实际值。不过,该字段不会被移除,因此仍可以通过反射与之交互。
这样的属性也可以用在注解中:
kotlin
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }
延迟初始化属性和变量
通常情况下,声明为非空类型的属性必须在构造函数中进行初始化。然而,很多时候这样做并不方便。例如,属性可以通过依赖注入来初始化,或者在单元测试的设置方法中进行初始化。在这些情况下,你无法在构造函数中提供一个非空的初始值,但你仍然希望在类的内部引用该属性时避免进行空检查。
为了处理这种情况,你可以使用 lateinit
修饰符来标记属性:
kotlin
public class MyTest {
lateinit var subject: TestSubject
@SetUp fun setup() {
subject = TestSubject()
}
@Test fun test() {
subject.method() // 直接解引用。
}
}
这个修饰符可以用于类体内部声明的 var
属性(不能用于主构造函数中的属性,并且仅适用于没有自定义 getter
或 setter
的属性),也适用于顶级属性和局部变量。属性或变量的类型必须是非空类型,并且不能是基本数据类型。
在延迟初始化属性尚未初始化时访问它会抛出一个特殊的异常,该异常会明确指出被访问的属性以及它尚未初始化这一事实。
要检查一个使用 lateinit
修饰的变量是否已经被初始化,可以对该属性的引用使用 .isInitialized
:
kotlin
if (foo::bar.isInitialized) {
println(foo.bar)
}
kotlin
class MyClass {
lateinit var myProperty: String
fun initialize() {
myProperty = "First value"
}
fun update() {
myProperty = "Updated value"
}
fun printProperty() {
if (::myProperty.isInitialized) {
println(myProperty)
}
}
}
fun main() {
val obj = MyClass()
obj.initialize()
obj.printProperty() // First value
obj.update()
obj.printProperty() // Updated value
}
这种检查方式仅适用于那些在词法上可访问的属性,具体来说,这些属性需满足以下条件:它们要么是在同一类型中声明的,要么是在某个外部类型中声明的,又或者是在同一文件的顶级作用域中声明的。