Kotlin, set var/val once to make it final, is that possible
Asked Answered
F

6

10

In Kotlin, we have val that is final and can't be change. e.g.

val something = "Something"

If a value that is is initialized later, we use lateinit var.

lateinit var something: String

But this is var instead of val. I wanted to set something once (not in constructor), and have it as final. How could I achieve this?

Fireproofing answered 19/2, 2018 at 1:26 Comment(0)
U
5

Reading into the conventions of Kotlin, a late-initialized variable which is final is impossible.

Consider its use case:

Normally, properties declared as having a non-null type must be initialized in the constructor. However, fairly often this is not convenient. For example, properties can be initialized through dependency injection, or in the setup method of a unit test. In this case, you cannot supply a non-null initializer in the constructor, but you still want to avoid null checks when referencing the property inside the body of a class.

lateinit var is providing relative sanity when dealing with a variable that may not have yet been initialized, such as the case with injected fields (like Spring and @Autowired). Then, speaking strictly in the context of dependency injection, if you don't have a way to concretely instantiate the variable at compile time, then you cannot leave it as a final field.

From a Java to Kotlin world, having a late initialized variable come in as final would look as paradoxical as this from Spring:

@Autowired
private final Interface something;
Unashamed answered 19/2, 2018 at 1:44 Comment(1)
Well elaborate.Drawl
U
2

What do you think the behavior should be when you attempt to set it again? Do you expect this to be enforced at compile time? Should it cause a crash at runtime or just do nothing?

If you expect it to happen at compile time, I'm pretty sure it's not possible for a compiler to catch something like that.

If you want some other behavior, you can make it a private variable with a public set method that does whatever you want if it's been already set.

Or you could encapsulate it in an instance of a custom class that does whatever behavior you want.

Unsaddle answered 19/2, 2018 at 2:28 Comment(0)
A
1

You can use following delegate class:

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!!
    }
}

Usage:

var example by WriteOnce<String>()

If you try to write a to the variable a second time it will produce a RuntimeException:

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

Not having any value also produces an exception, similar to as if you were using lateinit:

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

Which means this is val and lateinit combined because you can set the value at any time, only once ever.

The downside to this implementation is that this is not checked at compile time, meaning that you will only ever see errors in runtime. If that's acceptable in your use case, it certainly would be a good solution for what you're looking for.

For me this is more of a way to make sure that a variable is only every assigned once by code I control – something I can catch during testing as well as in production as a way to improve security by preventing foreign code from changing a variable.

Addiel answered 7/5, 2022 at 17:43 Comment(1)
I have published this delegate in my library: github.com/xdevs23/kboring/blob/main/src/main/kotlin/dev/…Addiel
P
1

use Delegated properties

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

usage:

var value:Int by Delegates.once()
Pirbhai answered 12/4, 2023 at 4:29 Comment(0)
I
0

It is possible you can use You can create a custom delegate for the property that is a combination of the existing notNull delegate and your own idea of set once. Read more about property delegates for information on how to create a custom one that can do whatever you want, including the use case you want here. You would then not use lateinit but instead this delegate.

Immemorial answered 19/2, 2018 at 17:53 Comment(0)
B
0

You can try to have another val initialize itself with by lazy with reading from lateinit, while keeping lateinit field private:

val something by lazy(LazyThreadSafetyMode.NONE) {
    _something
}
private lateinit var _something: String

This will not save you from assigning _something second time, but it will help with limiting the scope.

Bibbie answered 22/8 at 23:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.