属性
声明属性
在 Kotlin 类中,属性可以分为可变(读写)和只读两种类型。可变属性使用 var
关键字声明,而只读属性则使用 val
关键字声明。
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
var state: String? = null
var zip: String = "123456"
}
要使用属性,只需通过其名称引用即可:
fun copyAddress(address: Address): Address {
val result = Address() // 在 Kotlin 中没有 'new' 关键字
result.name = address.name // 调用访问器
result.street = address.street
// ...
return result
}
Getters 和 Setters
声明属性的完整语法如下:
var <propertyName>[: <属性类型>] [= <属性初始化值>]
[<getter>]
[<setter>]
属性的初始化程序、getter 和 setter 都是可选的。 如果属性的类型可以从初始化程序或 getter 的返回类型中推断出来,那么在属性声明中可以省略类型信息,如下所示:
var initialized = 1 // 类型为 Int,具有默认的 getter 和 setter
// var allByDefault // 错误:需要显式初始化程序,但具有默认的 getter 和 setter
只读属性声明的完整语法与可变属性的语法有两点不同:以 val
关键字开头而不是 var
,并且不允许包含 setter 方法:
val simple: Int? // 类型为 Int,具有默认的 getter,必须在构造函数中初始化
val inferredType = 1 // 类型为 Int,并且具有默认的 getter
您可以为属性定义自定义访问器。如果定义了自定义 getter,每次访问属性时都会调用它(这样你可以实现一个计算属性)。 下面是一个自定义 getter 的示例:
//sampleStart
class Rectangle(val width: Int, val height: Int) {
val area: Int // 属性类型是可选的,因为它可以从 getter 的返回类型中推断出来
get() = this.width * this.height
}
//sampleEnd
fun main() {
val rectangle = Rectangle(3, 4)
println("Width=${rectangle.width}, height=${rectangle.height}, area=${rectangle.area}")
}
如果可以从 getter 推断出来,可以省略属性类型:
val area get() = this.width * this.height
如果定义了自定义 setter,每次为属性赋值(除了初始化)时都会调用它。自定义 setter 如下:
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // 解析字符串并将值分配给其他属性
}
按照惯例,setter 参数的名称为 value
,但如果喜欢,可以选择其他名称。
如果需要为访问器添加注释或更改其可见性,但又不想更改默认实现,可以定义访问器而不定义其主体:
var setterVisibility: String = "abc"
private set // setter 是私有的,并且具有默认的实现
var setterWithAnnotation: Any? = null
@Inject set // 用 Inject 注释 setter
幕后字段
在 Kotlin 中,字段仅作为属性的一部分,用于在内存中保存属性的值。 字段不能直接声明。 然而,当属性需要一个幕后字段时,Kotlin 会自动提供。 这个幕后字段可以在访问器中使用 field
标识符引用:
var counter = 0 // 初始化程序直接分配给了幕后属性
set(value) {
if (value >= 0)
// 正确用法
field = value // 使用 'field' 代替 'counter' 本身
// 错误用法
// counter = value // 错误:堆栈溢出,使用实际名称 'counter' 将使 setter 递归调用
}
field
标识符只能在属性的访问器中使用。
如果属性使用至少一个访问器的默认实现,或者如果自定义访问器通过 field
标识符引用它,将会为该属性生成一个幕后字段。
例如,在以下情况下将不会有幕后字段:
val isEmpty: Boolean
get() = this.size == 0
幕后属性
如果你想要做一些不符合这个隐式幕后字段方案的事情,你总是可以退而使用一个幕后属性:
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")
}
编译时常量
如果一个只读属性的值在编译时已知,请使用 const
修饰符将其标记为编译时常量 。这样的属性需要满足以下要求:
编译器会将代码中的内联常量替换为其实际值,但这并不会移除定义的常量本身,因此仍然可以使用反射与之交互。
这样的属性也可以在注解中使用:
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }
实际编译成:
@NotNull
public static final String SUBSYSTEM_DEPRECATED = "This subsystem is deprecated";
@Deprecated(
message = "This subsystem is deprecated"
)
public static final void foo() { ... }
可以发现, @Deprecated
中的 message
被替换成了实际值,常量 SUBSYSTEM_DEPRECATED
也没有被删除。
延迟初始化属性和变量
通常,声明为非空类型的属性在构造函数中必须初始化。然而,有时情况并不方便。 例如,属性可能通过依赖注入进行初始化,或者在单元测试的设置方法中初始化。 在这些情况下,你无法在构造函数中提供非空初始化程序,但仍然希望在类体内引用属性时避免空检查。
为处理这类情况,可以使用 lateinit
修饰符标记属性:
public class MyTest {
lateinit var subject: TestSubject
@SetUp fun setup() {
subject = TestSubject()
}
@Test fun test() {
subject.method() // 直接引用
}
}
此修饰符可用于在类体中声明的 var
属性(不在主构造函数中,且属性没有自定义的 getter 或 setter),以及顶层属性和局部变量。 属性或变量的类型必须是非空的,且不能是原始类型。
在初始化之前访问 lateinit
属性会引发一个特殊的异常,清晰地标识正在访问的属性以及它尚未被初始化的事实。
检查 lateinit var
是否已初始化
要检查 lateinit var
是否已经初始化,可以在对该属性的引用上使用 .isInitialized
:
if (foo::bar.isInitialized) {
println(foo.bar)
}
此检查仅适用于在词法上可访问的属性,并符合以下声明之一时:
委托属性
最常见的一类属性就是简单地从幕后字段中读取(也可能是写入),但使用自定义的 getter 和 setter 允许你实现属性的任何行为。 介于第一种的简单性和第二种的多样性之间,存在一些属性可以做什么的通用模式。 一些例子包括:延迟加载的值、通过给定键从映射中读取值、访问数据库,以及在访问时通知监听器。
这些常见行为可以使用委托属性作为库来实现。
Last modified: 26 十一月 2024