Kotlin 语言参考文档 中文版 Help

中级教程: 属性

在初学者教程中, 你已经学习了如何使用属性来声明类实例的特征, 以及如何访问属性. 在这一章中, 我们进一步深入介绍 Kotlin 中的属性如何工作, 并探索在代码中使用属性的其它方式.

后端域变量(Backing Field)

在 Kotlin 中, 属性拥有默认的 get()set() 函数, 称为属性访问器, 负责获取和修改属性值. 这些默认函数在代码中并不明确的可见, 编译器自动生成这些函数, 在后台管理属性的访问. 这些访问器使用一个 后端域变量(Backing Field) 来存储实际的属性值.

如果以下条件中的任何一个成立, 后端域变量就会存在:

  • 你对属性使用默认的 get()set() 函数.

  • 你在代码中使用 field 关键字访问属性值.

例如, 这段代码有一个 category 属性, 它没有自定义的 get()set() 函数, 因此使用默认的实现:

class Contact(val id: Int, var email: String) { val category: String = "" }

在底层实现中, 这段代码等价于下面的伪代码:

class Contact(val id: Int, var email: String) { val category: String = "" get() = field set(value) { field = value } }

在这个示例中:

  • get() 函数从域变量获取属性值: "".

  • set() 函数接受参数 value, 并将它赋值给域变量, 其中 value"".

当你想要在你的 get()set() 函数中添加额外的逻辑, 又不引起无限的循环, 访问后端域变量会很有用. 例如, 你有一个 Person 类, 它有一个 name 属性:

class Person { var name: String = "" }

你想要确保 name 属性的首字母为大写, 因此创建了一个自定义 set() 函数, 它使用 .replaceFirstChar().uppercase() extension 函数. 但是, 如果在你的 set() 函数中直接引用属性, 就会导致无限循环, 并在运行期发生 StackOverflowError 错误:

class Person { var name: String = "" set(value) { // 这里会导致运行期错误 name = value.replaceFirstChar { firstChar -> firstChar.uppercase() } } } fun main() { val person = Person() person.name = "kodee" println(person.name) // 这里会发生错误: Exception in thread "main" java.lang.StackOverflowError }

要解决这个问题, 可以在你的 set() 函数中改为通过 field 关键字引用后端域变量:

class Person { var name: String = "" set(value) { field = value.replaceFirstChar { firstChar -> firstChar.uppercase() } } } fun main() { val person = Person() person.name = "kodee" println(person.name) // 输出结果为: Kodee }

当你想要添加日志, 在属性值变更时发送通知, 或者使用附加逻辑比较属性的旧值和新值时, 后端域变量也很有用.

详情请参见 后端域变量.

扩展属性

和扩展函数一样, 也有扩展属性. 扩展属性让你能够向既有的类添加新的属性, 而不必修改它们的源代码. 但是, Kotlin 中的扩展属性 没有 后端域变量. 这就意味着你需要自己编写 get()set() 函数. 此外, 没有后端域变量也意味着扩展属性不能保存任何状态.

要声明一个扩展属性, 请在你想要扩展的类的名称之后加上 ., 再加上属性的名称. 和通常的类属性一样, 你需要为你的属性声明接受者类型. 例如:

val String.lastChar: Char

当你想要属性包含计算得到的值, 而不使用继承时, 扩展属性是很有用的. 你可以将扩展属性想象为一个函数, 只有一个参数: 接受者对象.

例如, 假设你有一个数据类 Person, 它有 2 个属性: firstNamelastName.

data class Person(val firstName: String, val lastName: String)

你想要得到人的全名, 但不要修改 Person data 类, 也不要继承它. 你可以创建一个带有自定义 get() 函数的扩展属性来实现这一点:

data class Person(val firstName: String, val lastName: String) // 扩展属性, 用于得到全名 val Person.fullName: String get() = "$firstName $lastName" fun main() { val person = Person(firstName = "John", lastName = "Doe") // 使用扩展属性 println(person.fullName) // 输出结果为: John Doe }

与扩展函数一样, Kotlin 标准库大量使用了扩展属性. 例如, 请参见 CharSequencelastIndex 属性.

委托属性(Delegated Property)

类与接口 章节中, 你已经学习了委托. 你也可以对属性使用委托, 将它们的属性访问器委托给另一个对象. 当你的需求比存储属性更加复杂, 简单的后端域变量无法处理时, 委托属性会很有用, 例如需要将值存储到数据表中, 浏览器会话中, 或 Map 中. 使用委托属性(Delegated Property) 也可以减少样板代码, 因为取得和设置你的属性的逻辑只存在于你委托的对象中.

委托属性的语法与类的委托类似, 但操作层级不同. 请声明你的属性, 后面加上 by 关键字, 再加上你想要委托的对象. 例如:

val displayName: String by Delegate

这里, 委托属性 displayName 使用 Delegate 对象作为它的属性访问器.

你委托的每个对象 必须 有一个 getValue() 操作符函数, Kotlin 使用它来获取委托属性的值. 如果属性是可变的, 还必须有一个 setValue() 操作符函数, Kotlin 使用它来设置委托属性的值.

默认情况下, getValue()setValue() 函数的结构如下:

operator fun getValue(thisRef: Any?, property: KProperty<*>): String {} operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {}

在这些函数中:

  • operator 关键字将这些函数标记为操作符函数, 允许它们覆盖 get()set() 函数.

  • thisRef 参数表示 包含 委托属性的对象. 默认情况下, 类型设置为 Any?, 但你可能需要声明更具体的类型.

  • property 参数表示值正在被访问或被修改的那个属性. 你可以使用这个参数来获取属性信息, 例如属性的名称或类型. 默认情况下, 类型设置为 KProperty<*>. 在你的代码中, 不必进行修改.

getValue() 函数的返回类型默认为 String, 但如果你需要, 可以调整这个类型.

setValue() 函数有一个额外的参数 value, 用来保存正在赋值给属性的新值.

那么, 在实际运用中是什么样的呢? 假设你想要一个计算得到的属性, 例如用户的显示名称, 它只计算一次, 因为这个操作性能开销较大, 而你的应用程序对性能比较敏感. 你可以使用一个委托属性来缓存显示名称, 让它只计算一次, 但可以随时读取, 而不带来性能损失.

首先, 你需要创建负责委托的对象. 在这个示例中, 对象将是 CachedStringDelegate 类的一个实例:

class CachedStringDelegate { var cachedValue: String? = null }

cachedValue 属性包含缓存的值. 在 CachedStringDelegate 类中, 将你在委托属性的 get() 函数中想要的行为, 添加到 getValue() 操作符函数的 body 部:

class CachedStringDelegate { var cachedValue: String? = null operator fun getValue(thisRef: Any?, property: KProperty<*>): String { if (cachedValue == null) { cachedValue = "Default Value" println("Computed and cached: $cachedValue") } else { println("Accessed from cache: $cachedValue") } return cachedValue ?: "Unknown" } }

getValue() 函数检查 cachedValue 属性是否为 null. 如果是, 函数将它赋值为 "Default value", 并打印输出一个字符串, 作为日志. 如果 cachedValue 属性已经有了计算的值, 那么属性不为 null. 这种情况下, 并打印输出另一个字符串, 作为日志. 最后, 函数使用 Elvis 操作符, 返回缓存的值, 或者如果值为 null, 则返回 "Unknown".

现在你可以将想要缓存的属性(val displayName )委托给 CachedStringDelegate 类的实例:

import kotlin.reflect.KProperty class CachedStringDelegate { var cachedValue: String? = null operator fun getValue(thisRef: User, property: KProperty<*>): String { if (cachedValue == null) { cachedValue = "${thisRef.firstName} ${thisRef.lastName}" println("Computed and cached: $cachedValue") } else { println("Accessed from cache: $cachedValue") } return cachedValue ?: "Unknown" } } class User(val firstName: String, val lastName: String) { val displayName: String by CachedStringDelegate() } fun main() { val user = User("John", "Doe") // 第 1 次访问属性时, 计算值, 并缓存 println(user.displayName) // 输出结果为: Computed and cached: John Doe // 输出结果为: John Doe // 后续访问属性时, 会从缓存获取值 println(user.displayName) // 输出结果为: Accessed from cache: John Doe // 输出结果为: John Doe }

在这个示例中:

  • 创建一个 User 类, 它的 header 部有 2 个属性, firstName, 和 lastName, body 部有 1 个 属性, displayName.

  • displayName 属性委托给 CachedStringDelegate 类的实例.

  • 创建 User 类的一个实例 user.

  • 打印输出对 user 实例访问 displayName 属性的结果.

请注意, 在 getValue() 函数中, thisRef 参数的类型从 Any? 类型缩小到了对象类型: User. 这是为了让编译器能够访问 User 类的 firstNamelastName 属性.

标准委托

Kotlin 标准库提供了一些有用的委托, 让你不必总是从头创建. 如果你使用这些委托, 你不需要定义 getValue()setValue() 函数, 因为标准库会自动提供.

延迟加载(Lazy)属性

为了只在初次访问时才初始化一个属性, 请使用延迟加载(Lazy)属性. 标准库为委托提供了 Lazy 接口.

要创建 Lazy 接口的实例, 请使用 lazy() 函数, 给它提供一个 Lambda 表达式, get() 函数第一次被调用时会执行这个 Lambda 表达式. 之后对 get() 函数的任何调用都会返回与第一次调用时提供的相同结果. 延迟加载属性使用 尾缀 Lambda 表达式(Trailing Lambda) 语法来传递 Lambda 表达式.

例如:

class Database { fun connect() { println("Connecting to the database...") } fun query(sql: String): List<String> { return listOf("Data1", "Data2", "Data3") } } val databaseConnection: Database by lazy { val db = Database() db.connect() db } fun fetchData() { val data = databaseConnection.query("SELECT * FROM data") println("Data: $data") } fun main() { // 第 1 次访问 databaseConnection fetchData() // 输出结果为: Connecting to the database... // 输出结果为: Data: [Data1, Data2, Data3] // 后续访问, 会使用已有的连接 fetchData() // 输出结果为: Data: [Data1, Data2, Data3] }

在这个示例中:

  • 有一个 Database 类,它有 connect()query() 成员函数.

  • connect() 函数向控制台打印输出一个字符串, query() 函数接受一个 SQL 查询, 返回一个 List.

  • 有一个 databaseConnection 属性, 它是延迟加载属性.

  • lazy() 函数提供的 Lambda 表达式:

    • 创建一个 Database 类实例.

    • 对这个实例(db )调用 connect() 成员函数.

    • 返回这个实例.

  • 有一个 fetchData() 函数:

    • databaseConnection 属性调用 query() 函数, 创建一个 SQL 查询.

    • 将 SQL 查询赋值给 data 变量.

    • data 变量打印输出到控制台.

  • main() 函数调用 the fetchData() 函数. 第 1 次被调用时, 延迟加载属性会被初始化. 第 2 次被调用时, 会返回与第 1 次调用相同的结果.

延迟加载属性不仅对资源密集型的初始化有用, 而且对于你的代码中可能不会用到的属性也很有用. 此外, 延迟加载属性默认是线程安全的, 这一点对于并发环境尤其有用.

详情请参见 延迟加载属性.

可观察(Observable)属性

要监测属性值的变更, 请使用可观察(Observable)属性. 可观察属性 is useful when 如果你想要监测属性值的变更, 并利用这个信息来触发某种反应, 可观察属性会很有用. 标准库提供了 Delegates 对象可以用作委托.

要创建一个可观察属性, 你首先要导入 kotlin.properties.Delegates.observable. 然后, 使用 observable() 函数, 并为这个函数提供一个 Lambda 表达式, 当属性发生变更时会执行这个 Lambda 表达式. 与延迟加载属性一样, 可观察属性使用 尾缀 Lambda 表达式(Trailing Lambda) 语法来传递 Lambda 表达式.

例如:

import kotlin.properties.Delegates.observable class Thermostat { var temperature: Double by observable(20.0) { _, old, new -> if (new > 25) { println("Warning: Temperature is too high! ($old°C -> $new°C)") } else { println("Temperature updated: $old°C -> $new°C") } } } fun main() { val thermostat = Thermostat() thermostat.temperature = 22.5 // 输出结果为: Temperature updated: 20.0°C -> 22.5°C thermostat.temperature = 27.0 // 输出结果为: Warning: Temperature is too high! (22.5°C -> 27.0°C) }

在这个示例中:

  • 有一个 Thermostat 类, 包含一个可观察属性: temperature.

  • observable() 函数接受参数 20.0, 并将使用它来初始化属性.

  • 提供给 observable() 函数的 Lambda 表达式:

    • 有 3 个参数:

      • _, 表示属性本身.

      • old, 表示属性的旧值.

      • new, 表示属性的新值.

    • 检查 new 参数是否大于 25, 根据检查结果, 向控制台打印输出一个字符串.

  • main() 函数:

    • 创建 Thermostat 类的一个实例 thermostat.

    • 将实例的 temperature 属性值更新到 22.5, 这时会触发温度更新信息的打印输出语句.

    • 将实例的 temperature 属性值更新到 27.0, 这时会触发警告信息的打印输出语句.

可观察属性不仅可用于日志输出和调试目的. 还可以用于其它使用场景, 例如UI 更新, 或执行额外检查, 例如验证数据有效性.

详情请参见 可观察属性.

实际练习

习题 1

你管理着一家书店的库存系统. 库存信息保存在一个 List 中, 其中的每个元素表示某种书的数量. 例如, listOf(3, 0, 7, 12) 表示书店中第 1 种书有 3 份, 第 2 种书有 0 份, 第 3 种书有 7 份, 第 4 种书有 12 份.

请编写一个函数 findOutOfStockBooks(), 返回一个 List, 其中包含所有缺货书籍的索引.

提示 1

使用标准库中的 indices 扩展属性.

提示 2

你可以使用 buildList() 函数来创建和管理 List, 而不是手动的创建并返回一个可变的 List. buildList() 函数使用一个带接受者的 Lambda 表达式, 你在前面的章节中已经学过.

fun findOutOfStockBooks(inventory: List<Int>): List<Int> { // 请在这里编写你的代码 } fun main() { val inventory = listOf(3, 0, 7, 0, 5) println(findOutOfStockBooks(inventory)) // 输出结果为: [1, 3] }
fun findOutOfStockBooks(inventory: List<Int>): List<Int> { val outOfStockIndices = mutableListOf<Int>() for (index in inventory.indices) { if (inventory[index] == 0) { outOfStockIndices.add(index) } } return outOfStockIndices } fun main() { val inventory = listOf(3, 0, 7, 0, 5) println(findOutOfStockBooks(inventory)) // 输出结果为: [1, 3] }
fun findOutOfStockBooks(inventory: List<Int>): List<Int> = buildList { for (index in inventory.indices) { if (inventory[index] == 0) { add(index) } } } fun main() { val inventory = listOf(3, 0, 7, 0, 5) println(findOutOfStockBooks(inventory)) // 输出结果为: [1, 3] }

习题 2

你有一个旅行 App, 需要以公里和英里为单位显示距离. 请为 Double 类型创建一个扩展属性 asMiles, 将距离从公里转换为英里:

提示

请记住, 扩展属性需要自定义的 get() 函数.

val // 请在这里编写你的代码 fun main() { val distanceKm = 5.0 println("$distanceKm km is ${distanceKm.asMiles} miles") // 输出结果为: 5.0 km is 3.106855 miles val marathonDistance = 42.195 println("$marathonDistance km is ${marathonDistance.asMiles} miles") // 输出结果为: 42.195 km is 26.218757 miles }
val Double.asMiles: Double get() = this * 0.621371 fun main() { val distanceKm = 5.0 println("$distanceKm km is ${distanceKm.asMiles} miles") // 输出结果为: 5.0 km is 3.106855 miles val marathonDistance = 42.195 println("$marathonDistance km is ${marathonDistance.asMiles} miles") // 输出结果为: 42.195 km is 26.218757 miles }

习题 3

你有一个系统健康状况检查器, 能够检查云系统的状态. 它有 2 个函数用来执行健康状况检查, 但是这 2 个函数会消耗大量性能. 请使用延迟加载属性来初始化这些检查, 让这些性能消耗巨大的函数只在需要是运行:

fun checkAppServer(): Boolean { println("Performing application server health check...") return true } fun checkDatabase(): Boolean { println("Performing database health check...") return false } fun main() { // 请在这里编写你的代码 when { isAppServerHealthy -> println("Application server is online and healthy") isDatabaseHealthy -> println("Database is healthy") else -> println("System is offline") } // 输出结果为: Performing application server health check... // 输出结果为: Application server is online and healthy }
fun checkAppServer(): Boolean { println("Performing application server health check...") return true } fun checkDatabase(): Boolean { println("Performing database health check...") return false } fun main() { val isAppServerHealthy by lazy { checkAppServer() } val isDatabaseHealthy by lazy { checkDatabase() } when { isAppServerHealthy -> println("Application server is online and healthy") isDatabaseHealthy -> println("Database is healthy") else -> println("System is offline") } // 输出结果为: Performing application server health check... // 输出结果为: Application server is online and healthy }

习题 4

你正在构建一个简单的预算追踪 App. App 需要监测用户预算余额的变化, 并在余额低于某个阈值时通知用户. 你有一个 Budget 类, 使用 totalBudget 属性初始化, 这个属性包含预算初始金额. 请在这个类中创建一个可观察属性 remainingBudget, 它需要:

  • 当余额低于预算初始金额的 20% 时, 打印输出一个警告信息.

  • 当预算高于前一个值时, 打印输出一个鼓励信息.

import kotlin.properties.Delegates.observable class Budget(val totalBudget: Int) { var remainingBudget: Int // 请在这里编写你的代码 } fun main() { val myBudget = Budget(totalBudget = 1000) myBudget.remainingBudget = 800 myBudget.remainingBudget = 150 // 输出结果为: Warning: Your remaining budget (150) is below 20% of your total budget. myBudget.remainingBudget = 50 // 输出结果为: Warning: Your remaining budget (50) is below 20% of your total budget. myBudget.remainingBudget = 300 // 输出结果为: Good news: Your remaining budget increased to 300. }
import kotlin.properties.Delegates.observable class Budget(val totalBudget: Int) { var remainingBudget: Int by observable(totalBudget) { _, oldValue, newValue -> if (newValue < totalBudget * 0.2) { println("Warning: Your remaining budget ($newValue) is below 20% of your total budget.") } else if (newValue > oldValue) { println("Good news: Your remaining budget increased to $newValue.") } } } fun main() { val myBudget = Budget(totalBudget = 1000) myBudget.remainingBudget = 800 myBudget.remainingBudget = 150 // 输出结果为: Warning: Your remaining budget (150) is below 20% of your total budget. myBudget.remainingBudget = 50 // 输出结果为: Warning: Your remaining budget (50) is below 20% of your total budget. myBudget.remainingBudget = 300 // 输出结果为: Good news: Your remaining budget increased to 300. }

下一步

中级教程: Null 值安全性

2025/08/04