5.8.10.1 属性
原文链接: https://kotlinlang.org/docs/properties.html
5.8.10.1 属性
在 Kotlin 中,属性让你无需编写访问或修改数据的函数,就能存储和管理数据。你可以在类、接口、对象、伴生对象中使用属性,也可以在这些结构之外把它们声明为顶层属性。
每个属性都有一个名称、一个类型,以及一个自动生成的 get() 函数(称为 getter)。你可以用 getter 读取属性的值。如果属性是可变的,它还有一个 set() 函数(称为 setter),用于修改属性的值。
提示: getter 和 setter 统称为访问器。
声明属性
属性可以是可变的(var)或只读的(val)。你可以在 .kt 文件中把它们声明为顶层属性。可以把顶层属性理解为属于某个包的全局变量:
1
2
3
4
5
| // 文件:Constants.kt
package my.app
val pi = 3.14159
var counter = 0
|
你也可以在类、接口或对象中声明属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 带属性的类
class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
}
// 带属性的接口
interface ContactInfo {
val email: String
}
// 带属性的对象
object Company {
var name: String = "Detective Inc."
val country: String = "UK"
}
// 实现该接口的类
class PersonContact : ContactInfo {
override val email: String = "sherlock@example.com"
}
|
要使用属性,请按名称引用它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| class Address {
var name: String = "Holmes, Sherlock"
var street: String = "Baker"
var city: String = "London"
}
interface ContactInfo {
val email: String
}
object Company {
var name: String = "Detective Inc."
val country: String = "UK"
}
class PersonContact : ContactInfo {
override val email: String = "sherlock@example.com"
}
fun copyAddress(address: Address): Address {
val result = Address()
// 访问 result 实例中的属性
result.name = address.name
result.street = address.street
result.city = address.city
return result
}
fun main() {
val sherlockAddress = Address()
val copy = copyAddress(sherlockAddress)
// 访问 copy 实例中的属性
println("Copied address: ${copy.name}, ${copy.street}, ${copy.city}")
// Copied address: Holmes, Sherlock, Baker, London
// 访问 Company 对象中的属性
println("Company: ${Company.name} in ${Company.country}")
// Company: Detective Inc. in UK
val contact = PersonContact()
// 访问 contact 实例中的属性
println("Email: ${contact.email}")
// Email: sherlock@email.com
}
|
在 Kotlin 中,我们建议在声明属性时就初始化它,这样代码更安全、更易读。不过在特殊情况下,你也可以稍后再初始化它们。
如果编译器能从初始化器或 getter 的返回类型推断出类型,属性类型可以省略:
1
2
| var initialized = 1 // 推断出的类型是 Int
var allByDefault // 错误:属性必须初始化。
|
自定义 getter 和 setter
默认情况下,Kotlin 会自动生成 getter 和 setter。当你需要额外逻辑(例如校验、格式化,或基于其他属性进行计算)时,可以定义自己的自定义访问器。
自定义 getter 会在每次访问该属性时运行:
1
2
3
4
5
6
7
8
| class Rectangle(val width: Int, val height: Int) {
val area: Int
get() = this.width * this.height
}
fun main() {
val rectangle = Rectangle(3, 4)
println("Width=${rectangle.width}, height=${rectangle.height}, area=${rectangle.area}")
}
|
如果编译器能从 getter 推断出类型,可以省略类型:
1
| val area get() = this.width * this.height
|
自定义 setter 会在每次给属性赋值时运行(初始化时除外)。按照约定,setter 参数名是 value,但你也可以选择其他名称:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class Point(var x: Int, var y: Int) {
var coordinates: String
get() = "$x,$y"
set(value) {
val parts = value.split(",")
x = parts[0].toInt()
y = parts[1].toInt()
}
}
fun main() {
val location = Point(1, 2)
println(location.coordinates)
// 1,2
location.coordinates = "10,20"
println("${location.x}, ${location.y}")
// 10, 20
}
|
修改可见性或添加注解
在 Kotlin 中,你可以更改访问器的可见性,或添加注解,而无需替换默认实现。这些修改不必写在函数体 {} 中。
要更改访问器的可见性,请在 get 或 set 关键字之前使用修饰符:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| class BankAccount(initialBalance: Int) {
var balance: Int = initialBalance
// 只有该类可以修改 balance
private set
fun deposit(amount: Int) {
if (amount > 0) balance += amount
}
fun withdraw(amount: Int) {
if (amount > 0 && amount <= balance) balance -= amount
}
}
fun main() {
val account = BankAccount(100)
println("Initial balance: ${account.balance}")
// 100
account.deposit(50)
println("After deposit: ${account.balance}")
// 150
account.withdraw(70)
println("After withdrawal: ${account.balance}")
// 80
// account.balance = 1000
// 错误:无法赋值,因为 setter 是 private 的
}
|
要为访问器添加注解,请在 get 或 set 关键字之前使用注解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 定义一个可应用于 getter 的注解
@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class Inject
class Service {
var dependency: String = "Default Service"
// 为 getter 添加注解
@Inject get
}
fun main() {
val service = Service()
println(service.dependency)
// Default service
println(service::dependency.getter.annotations)
// [@Inject()]
println(service::dependency.setter.annotations)
// []
}
|
这个例子使用反射来展示 getter 和 setter 上存在哪些注解。
幕后字段
当值需要存储在内存中时,编译器会自动为属性生成幕后字段。
例如,当你使用默认的 get() 和 set() 函数时,编译器会创建幕后字段,因为它们要读写存储的值:
你可以在自定义的 get() 或 set() 函数中使用 field 关键字访问幕后字段。例如,你可以在 getter 或 setter 中添加额外逻辑,或在属性变化时触发额外动作。
在这个例子中,score 属性在 set() 函数内部使用了幕后字段,因此更新值的同时也会触发一条日志:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class Scoreboard {
var score: Int = 0
set(value) {
field = value
// 更新值时添加日志
println("Score updated to $field")
}
}
fun main() {
val board = Scoreboard()
board.score = 10
// Score updated to 10
board.score = 20
// Score updated to 20
}
|
并非所有属性默认都会创建幕后字段,因为它们可能并不需要。例如 isEmpty 属性就没有幕后字段,因为每次访问时它的值都由 size 属性计算得出:
1
2
| val isEmpty: Boolean
get() = this.size == 0
|
显式幕后字段
有时你可能需要更大的灵活性。例如,你有一个 API,希望在内部可以修改该属性,但在外部不行。这种情况下,可以使用显式幕后字段。
在下面的例子中,ShoppingCart 类有一个 items 属性,表示购物车中的所有内容。该类把 items 属性对外公开为只读字符串列表,但在内部通过显式幕后字段把数据保存在可变列表中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| class ShoppingCart {
// 带显式幕后字段的公开只读视图
val items: List<String>
field = mutableListOf()
fun addItem(item: String) {
items.add(item)
}
fun removeItem(item: String) {
items.remove(item)
}
}
fun main() {
val cart = ShoppingCart()
cart.addItem("Apple")
cart.addItem("Banana")
println(cart.items)
// [Apple, Banana]
cart.removeItem("Apple")
println(cart.items)
// [Banana]
}
|
在这个例子中,编译器从 mutableListOf() 调用推断出幕后字段的类型是 MutableList<String>。你也可以显式声明幕后字段的类型:
1
2
3
| val items: List<String>
// 带显式类型的显式幕后字段
field: MutableList<String> = mutableListOf()
|
在 ShoppingCart 类的例子中,编译器会把 items 属性智能转换为 MutableList<String> 类型,因此类内部可以通过 add() 和 remove() 函数增删购物车中的项。而在类外部,编译器使用公开的属性类型 List<String>,所以 API 使用者只能读取 items 列表中的内容。
限制
要使用显式幕后字段,其属性以及幕后字段本身必须遵循某些规则。属性只有在满足以下条件时才能有显式幕后字段:
- 没有自定义 getter。
- 是只读的(
val)。 - 不是
open。 - 不是委托属性。
- 不是编译期常量。
此外,幕后字段的类型必须是属性类型的子类型,并具有 private 可见性。
你可以改用幕后属性来绕开这些限制。
幕后属性
如果显式幕后字段不符合你的用例,可以试试称为幕后属性的编码模式。
例如,如果你的属性需要自定义 getter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| class UserDirectory {
private val _users = mutableListOf(
"sarah",
"mike",
"emma"
)
val users: List<String>
get() = _users.sorted()
fun addUser(username: String) {
_users.add(username)
}
}
fun main() {
val directory = UserDirectory()
directory.addUser("alex")
println(directory.users)
// [alex, emma, mike, sarah]
}
|
提示: 为幕后属性命名时使用前导下划线,以遵循 Kotlin 编码规范。
在这个例子中,UserDirectory 类有一个只读的 users 属性,列出目录中的所有用户。_users 变量是私有的幕后属性,保存真实列表。公开的 users 属性的 getter 会在返回前对条目排序。
编译期常量
如果只读属性的值在编译期已知,可以用 const 修饰符把它标记为编译期常量。编译期常量会在编译期被内联,因此每次引用都会被替换为它的实际值。由于不需要调用 getter,访问它们的效率更高:
1
2
3
4
5
| // 文件:AppConfig.kt
package com.example
// 编译期常量
const val MAX_LOGIN_ATTEMPTS = 3
|
编译期常量必须满足以下要求:
编译期常量仍然有幕后字段,因此你可以用反射与它们交互。
你也可以在注解中使用这些属性:
1
2
3
| const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun processLegacyOrders() { ... }
|
延迟初始化的属性与变量
通常你必须在构造函数中初始化属性。不过,这并不总是方便。例如,你可能通过依赖注入或在单元测试的 setup 方法中初始化属性。
为处理这些情况,请用 lateinit 修饰符标记属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
| public class OrderServiceTest {
lateinit var orderService: OrderService
@SetUp fun setup() {
orderService = OrderService()
}
@Test fun processesOrderSuccessfully() {
// 直接调用 orderService,不检查是否为 null
// 也不检查是否已初始化
orderService.processOrder()
}
}
|
你可以在声明为以下形式的 var 属性上使用 lateinit 修饰符:
对于类属性:
- 不能在主构造函数中声明它们。
- 它们不能有自定义 getter 或 setter。
在所有这些情况下,属性或变量都必须是非空的,且不能是基本类型。
如果在初始化之前访问 lateinit 属性,Kotlin 会抛出一个特定的异常,指出被访问的未初始化属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class ReportGenerator {
lateinit var report: String
fun printReport() {
// 由于在初始化之前访问,这里会抛出异常
// 初始化
println(report)
}
}
fun main() {
val generator = ReportGenerator()
generator.printReport()
// 线程 "main" 中的异常 kotlin.UninitializedPropertyAccessException: lateinit property report has not been initialized
}
|
要检查某个 lateinit var 是否已经初始化,请对该属性的引用使用 isInitialized 属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| class WeatherStation {
lateinit var latestReading: String
fun printReading() {
// 检查该属性是否已初始化
if (this::latestReading.isInitialized) {
println("Latest reading: $latestReading")
} else {
println("No reading available")
}
}
}
fun main() {
val station = WeatherStation()
station.printReading()
// No reading available
station.latestReading = "22°C, sunny"
station.printReading()
// Latest reading: 22°C, sunny
}
|
只有当你的代码已经可以访问某个属性时,才能对它使用 isInitialized。该属性必须声明在同一个类、外层类中,或作为同一文件中的顶层属性。
重写属性
请参阅重写属性。
委托属性
为了复用逻辑、减少代码重复,你可以把获取和设置属性的职责委托给一个单独的对象。
委托访问器行为可以让属性的访问器逻辑集中在一处,从而更易于复用。这种方式适合实现以下行为:
- 惰性计算值。
- 按键从映射中读取。
- 访问数据库。
- 在属性被访问时通知监听器。
你可以在库中自己实现这些常见行为,也可以使用外部库提供的现有委托。更多信息请参阅委托属性。