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

属性声明

属性可以使用 var 关键字声明为可变的,也可以使用 val 关键字声明为只读的。

kotlin 复制代码
class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
    var state: String? = null
    var zip: String = "123456"
}

要使用这些属性,只需通过属性名来引用它们。

kotlin 复制代码
fun copyAddress(address: Address): Address {
    val result = Address()

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

gettersetter

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

kotlin 复制代码
var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

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

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

  • val 开头而非 var

  • 不允许有 setter

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

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

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

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

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

kotlin 复制代码
class Rectangle(val width: Int, val height: Int) {
    val area get() = this.width * this.height
}

当你定义了一个自定义的 setter 方法后,除了属性初始化的时候,每次给该属性赋值都会调用这个自定义 setter

kotlin 复制代码
var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value)
    }

按照惯例,setter 参数的名称是 value,但如果你愿意,也可以选择其他名称。

如果你需要为访问器添加注解或更改其可见性,但又不想改变默认实现,那么可以只定义访问器而不定义其主体:

kotlin 复制代码
var setterVisibility: String = "abc"
    private set // 该 setter 是私有的,且采用默认实现。
kotlin 复制代码
var setterWithAnnotation: Any? = null
    @Inject set // 为该 setter 添加 Inject 注解。

幕后字段

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

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

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

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

field 标识符只能在属性的访问器中使用。

如果一个属性至少有一个访问器使用默认实现,或者自定义访问器通过 field 标识符引用它,那么就会为该属性生成一个幕后字段。

例如,在以下情况下就不会生成幕后字段:

kotlin 复制代码
val isEmpty: Boolean
    get() = this.size == 0

后备属性

如果你想实现一些无法通过隐式幕后字段机制完成的操作,那么你总可以采用后备属性的方式:

kotlin 复制代码
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

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

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

kotlin 复制代码
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

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

延迟初始化属性和变量

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

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

kotlin 复制代码
public class MyTest {
    lateinit var subject: TestSubject

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

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

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

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

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

kotlin 复制代码
if (foo::bar.isInitialized) {
    println(foo.bar)
}
kotlin 复制代码
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
}

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

相关推荐
Meteors.1 小时前
Android约束布局(ConstraintLayout)常用属性
android
alexhilton1 小时前
玩转Shader之学会如何变形画布
android·kotlin·android jetpack
whysqwhw6 小时前
安卓图片性能优化技巧
android
风往哪边走6 小时前
自定义底部筛选弹框
android
Yyyy4826 小时前
MyCAT基础概念
android
Android轮子哥7 小时前
尝试解决 Android 适配最后一公里
android
雨白8 小时前
OkHttp 源码解析:enqueue 非同步流程与 Dispatcher 调度
android
风往哪边走9 小时前
自定义仿日历组件弹框
android
没有了遇见9 小时前
Android 外接 U 盘开发实战:从权限到文件复制
android
Monkey-旭10 小时前
Android 文件存储机制全解析
android·文件存储·kolin