Kotlin,设置一次 var/val 使其成为最终的,这可能吗

问题描述 投票:0回答:6

在 Kotlin 中,我们有

val
,它是最终的且无法更改。例如

val something = "Something"

如果稍后初始化某个值,我们使用

lateinit var

lateinit var something: String

但这是

var
而不是
val
。我想设置一次(不在构造函数中),并将其作为最终的。我怎样才能实现这个目标?

kotlin final
6个回答
5
投票

阅读 Kotlin 的约定,一个最终的后期初始化变量是不可能的。

考虑其用例

通常,声明为非空类型的属性必须在构造函数中初始化。然而,这通常并不方便。例如,可以通过依赖项注入或在单元测试的设置方法中初始化属性。在这种情况下,您不能在构造函数中提供非 null 初始值设定项,但您仍然希望在引用类体内的属性时避免 null 检查。

在处理可能尚未初始化的变量时,

lateinit var
提供了相对的理智,例如注入字段的情况(如 Spring 和
@Autowired
)。 然后,严格来说,在依赖注入的上下文中,如果您没有有办法在编译时具体实例化变量,那么您不能将其保留为最终字段。

从 Java 到 Kotlin 世界,将后期初始化的变量作为 Final 引入看起来就像 Spring 中的这样自相矛盾:

@Autowired
private final Interface something;

2
投票

当您尝试再次设置它时,您认为行为应该是什么?您希望在编译时强制执行此操作吗?它应该在运行时导致崩溃还是什么也不做?

如果您期望它在编译时发生,我很确定编译器不可能捕获类似的东西。

如果您想要一些其他行为,您可以将其设为具有公共 set 方法的私有变量,如果已经设置了该方法,该方法可以执行您想要的任何操作。

或者您可以将其封装在自定义类的实例中,该类可以执行您想要的任何行为。


1
投票

您可以使用以下委托类:

import kotlin.reflect.KProperty

class WriteOnce<T> {
    private var holder = holdValue<T>()
    private var value by holder

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        if (!holder.hasValue) {
            throw IllegalStateException("Property must be initialized before use")
        }
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        if (holder.hasValue) {
            throw RuntimeException("Write-once property already has a value")
        }
        this.value = value
    }

}

fun <T> holdValue() = ValueHolder<T>()

class ValueHolder<T> {
    var value: T? = null
    var hasValue: Boolean = false
        private set

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        this.value = value
        hasValue = true
    }

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return this.value!!
    }
}

用途:

var example by WriteOnce<String>()

如果您尝试第二次向变量写入 a ,则会产生 RuntimeException:

java.lang.RuntimeException: Write-once property already has a value

没有任何值也会产生异常,类似于您使用

lateinit
:

java.lang.IllegalStateException: Property must be initialized before use

这意味着这是

val
lateinit
的组合,因为您可以随时设置该值,但只能设置一次。

此实现的缺点是在编译时不会检查,这意味着您只会在运行时看到错误。如果这在您的用例中是可以接受的,那么它肯定会是您正在寻找的一个很好的解决方案。

对我来说,这更像是一种确保变量仅由我控制的代码分配一次的方法——我可以在测试和生产过程中捕捉到这一点,作为通过防止外部代码更改变量来提高安全性的一种方法.


1
投票

使用委托属性

fun <T : Any> Delegates.once(): ReadWriteProperty<Any?, T> = object : ReadWriteProperty<Any?, T> {
    private var value: T? = null

    public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
    }

    public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        if (this.value != null) throw IllegalStateException("Property ${property.name} cannot be set more than once.")
        this.value = value
    }
}

用途:

var value:Int by Delegates.once()

0
投票

您可以使用您可以为该属性创建一个自定义委托,该属性是现有

notNull
委托和您自己的
set once
想法的组合。 阅读有关“属性委托”的更多信息,了解如何创建可以执行您想要的任何操作的自定义属性委托,包括您想要的用例。 然后您就不会使用 lateinit 而是使用这个委托。
    


0
投票
val

使用

by lazy
进行初始化,并从
lateinit
读取内容,同时保持
lateinit
字段私有:
val something by lazy(LazyThreadSafetyMode.NONE) {
    _something
}
private lateinit var _something: String

这不会让您免于第二次分配 
_something

,但有助于限制范围。

    

© www.soinside.com 2019 - 2025. All rights reserved.