Kotlin 2.1.0 入门教程(十六)属性、getter、setter、幕后字段、后备属性、编译时常量、延迟初始化

发布于:2025-02-13 ⋅ 阅读:(14) ⋅ 点赞:(0)

属性声明

属性可以使用 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()

    result.name = address.name
    result.street = address.street
    
    return result
}

gettersetter

属性声明的完整语法如下:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

这里的初始值设定项、gettersetter 都是可选的。如果属性的类型可以从初始值设定项或者 getter 的返回类型推导出来,那么属性类型也是可选的。

只读属性声明的完整语法和可变属性声明有两点不同:

  • val 开头而非 var

  • 不允许有 setter

// 类型为 Int,有默认的 getter,必须在构造函数中初始化。
val simple: Int?

// 类型为 Int,有默认的 getter。
val inferredType = 1

你可以为属性定义自定义的访问器。要是定义了自定义的 getter,每次访问该属性时都会调用它(这样就能实现一个计算属性)。下面是一个自定义 getter 的示例:

class Rectangle(val width: Int, val height: Int) {
    // 属性类型可选,因为能从 getter 的返回类型推导出来。
    val area: Int
        get() = this.width * this.height
}

如果属性类型可以从获取器 getter 中推断出来,那么你可以省略该属性的类型声明。

class Rectangle(val width: Int, val height: Int) {
    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 // 为该 setter 添加 Inject 注解。

幕后字段

字段仅作为属性的一部分,用于在内存中存储属性的值。字段不能直接声明。

不过,当属性需要一个幕后字段时,Kotlin 会自动提供。

在访问器中,可以使用 field 标识符来引用这个幕后字段:

var counter = 0 // 初始化器会直接为幕后字段赋值。
    set(value) {
        if (value >= 0)
            field = value
            // 错误,会导致栈溢出:使用实际名称 counter 会使 setter 递归调用。
            // counter = value
    }

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")
    }

JVM 上,对于使用默认 gettersetter 的私有属性,其访问操作会被优化,以避免函数调用带来的开销。

编译时常量

如果一个只读属性的值在编译时就已知,那么可以使用 const 修饰符将其标记为编译时常量。这样的属性需要满足以下要求:

  • 它必须是顶级属性,或者是 object 声明或伴生对象的成员。

  • 它必须用 String 类型或基本数据类型的值进行初始化。

  • 它不能有自定义的 getter

编译器会对常量的使用进行内联处理,将对常量的引用替换为其实际值。不过,该字段不会被移除,因此仍可以通过反射与之交互。

这样的属性也可以用在注解中:

const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }

延迟初始化属性和变量

通常情况下,声明为非空类型的属性必须在构造函数中进行初始化。然而,很多时候这样做并不方便。例如,属性可以通过依赖注入来初始化,或者在单元测试的设置方法中进行初始化。在这些情况下,你无法在构造函数中提供一个非空的初始值,但你仍然希望在类的内部引用该属性时避免进行空检查。

为了处理这种情况,你可以使用 lateinit 修饰符来标记属性:

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // 直接解引用。
    }
}

这个修饰符可以用于类体内部声明的 var 属性(不能用于主构造函数中的属性,并且仅适用于没有自定义 gettersetter 的属性),也适用于顶级属性和局部变量。属性或变量的类型必须是非空类型,并且不能是基本数据类型。

在延迟初始化属性尚未初始化时访问它会抛出一个特殊的异常,该异常会明确指出被访问的属性以及它尚未初始化这一事实。

要检查一个使用 lateinit 修饰的变量是否已经被初始化,可以对该属性的引用使用 .isInitialized

if (foo::bar.isInitialized) {
    println(foo.bar)
}
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
}

这种检查方式仅适用于那些在词法上可访问的属性,具体来说,这些属性需满足以下条件:它们要么是在同一类型中声明的,要么是在某个外部类型中声明的,又或者是在同一文件的顶级作用域中声明的。