属性
在 Kotlin 中,属性允许您存储和管理数据,而无需编写用于访问或修改数据的函数。 您可以在 类、接口、对象、伴生对象 中使用属性,甚至可以在这些结构之外将其作为顶层属性使用。
每个属性都有一个名称、一个类型,以及一个自动生成的名为 getter 的 get() 函数。您可以使用 getter 来读取属性的值。如果属性是可变的,它还有一个名为 setter 的 set() 函数,允许您修改属性的值。
Getter 和 setter 被称为 访问器。
声明属性
属性可以是可变的 (var) 或只读的 (val)。 您可以将它们作为顶层属性在 .kt 文件中声明。可以将顶层属性视为属于某个软件包的全局变量:
// 文件:Constants.kt
package my.app
val pi = 3.14159
var counter = 0您还可以在类、接口或对象内部声明属性:
// 带有属性的类
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 = "[email protected]"
}要使用属性,请通过其名称进行引用:
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 = "[email protected]"
}
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: [email protected]
}在 Kotlin 中,我们建议在声明属性时对其进行初始化,以保持代码安全且易于阅读。不过,在特殊情况下,您可以 稍后初始化它们。
如果编译器可以从初始值设定项或 getter 的返回值类型中推断出属性类型,则可以省略类型声明:
var initialized = 1 // 推断类型为 Int
var allByDefault // 错误:属性必须初始化自定义 getter 和 setter
默认情况下,Kotlin 会自动生成 getter 和 setter。当您需要额外的逻辑(例如验证、格式设置或基于其他属性的计算)时,可以定义自己的自定义访问器。
每次访问属性时都会运行自定义 getter:
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 中推断出类型,则可以省略类型:
val area get() = this.width * this.height除了初始化期间外,每次为您向属性赋值时,自定义 setter 都会运行。按照约定,setter 形参的名称是 value,但您可以选择不同的名称:
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 关键字之前使用修饰符:
class BankAccount(initialBalance: Int) {
var balance: Int = initialBalance
// 只有类本身可以修改余额
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 是私有的,无法赋值
}要为访问器添加注解,请在 get 或 set 关键字之前使用注解:
// 定义一个可以应用于 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 上存在哪些注解。
支持字段
在 Kotlin 中,访问器使用支持字段将属性的值存储在内存中。当您想要在 getter 或 setter 中添加额外逻辑,或者想要在属性更改时触发额外操作时,支持字段非常有用。
您不能直接声明支持字段。Kotlin 仅在必要时生成它们。您可以使用 field 关键字在访问器中引用支持字段。
只有在您使用默认的 getter 或 setter,或者在至少一个自定义访问器中使用 field 时,Kotlin 才会生成支持字段。
例如,isEmpty 属性没有支持字段,因为它使用了不包含 field 关键字的自定义 getter:
val isEmpty: Boolean
get() = this.size == 0在以下示例中,score 属性具有支持字段,因为 setter 使用了 field 关键字:
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
}幕后属性
有时您可能需要比 支持字段 提供的灵活性更高的方案。例如,如果您有一个 API,希望能够在内部修改属性但在外部不可修改。在这种情况下,您可以使用一种称为 幕后属性 的编码模式。
在以下示例中,ShoppingCart 类具有一个 items 属性,代表购物车中的所有商品。您希望 items 属性在类外部是只读的,但仍允许一种“经批准”的方式来直接修改 items 属性。为了实现这一点,您可以定义一个名为 _items 的私有幕后属性,以及一个名为 items 的公共属性,后者委托给幕后属性的值。
class ShoppingCart {
// 幕后属性
private val _items = mutableListOf<String>()
// 公共只读视图
val items: List<String>
get() = _items
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]
}在此示例中,用户只能通过 addItem() 函数向购物车添加商品,但仍可以访问 items 属性来查看其中的内容。
在为幕后属性命名时,请使用前导下划线,以符合 Kotlin 编码约定。
在 JVM 上,编译器会优化对具有默认访问器的私有属性的访问,以避免函数调用开销。
当您希望多个公共属性共享一个状态时,幕后属性也很有用。例如:
class Temperature {
// 存储摄氏温度的幕后属性
private var _celsius: Double = 0.0
var celsius: Double
get() = _celsius
set(value) { _celsius = value }
var fahrenheit: Double
get() = _celsius * 9 / 5 + 32
set(value) { _celsius = (value - 32) * 5 / 9 }
}
fun main() {
val temp = Temperature()
temp.celsius = 25.0
println("${temp.celsius}°C = ${temp.fahrenheit}°F")
// 25.0°C = 77.0°F
temp.fahrenheit = 212.0
println("${temp.celsius}°C = ${temp.fahrenheit}°F")
// 100.0°C = 212.0°F
}在此示例中,celsius 和 fahrenheit 属性都访问 _celsius 幕后属性。这种设置提供了一个单一的事实来源,并带有两个公共视图。
编译时常量
如果只读属性的值在编译时就已知,请使用 const 修饰符将其标记为 编译时常量。编译时常量会在编译时内联,因此每个引用都会被替换为其具体值。访问它们更高效,因为不会调用 getter:
// 文件:AppConfig.kt
package com.example
// 编译时常量
const val MAX_LOGIN_ATTEMPTS = 3编译时常量必须满足以下要求:
编译时常量仍然具有支持字段,因此您可以通过 反射 与其交互。
您还可以在注解中使用这些属性:
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun processLegacyOrders() { ... }延迟初始化属性和变量
通常,您必须在构造函数中初始化属性。然而,这并不总是很方便。例如,您可能通过依赖注入或在单元测试的设置方法中初始化属性。
为了处理这些情况,请使用 lateinit 修饰符标记属性:
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 会抛出一个特定的异常,该异常会指明正在访问的未初始化属性:
class ReportGenerator {
lateinit var report: String
fun printReport() {
// 由于在初始化之前访问,会抛出异常
println(report)
}
}
fun main() {
val generator = ReportGenerator()
generator.printReport()
// Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property report has not been initialized
}要检查 lateinit var 是否已初始化,请在该 属性的引用 上使用 isInitialized 属性:
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。该属性必须在同一个类中、在外层类中或作为同一个文件中的顶层属性声明。
重写属性
请参阅 重写属性。
委托属性
为了重用逻辑并减少代码重复,您可以将获取和设置属性的职责委托给一个单独的对象。
委托访问器行为可以使属性的访问器逻辑保持集中,从而更容易重用。这种方法在实现如下行为时非常有用:
- 延迟计算值。
- 通过给定的键从映射中读取。
- 访问数据库。
- 在访问属性时通知侦听器。
您可以自己在库中实现这些通用行为,也可以使用外部库提供的现有委托。更多信息请参阅 委托属性。
