Kotlin: lateinit to val, or, alternatively, a var that can set once
Asked Answered
S

12

54

Just curious: In Kotlin, I would love to get some val that can be initialized by lazy, but with a parameter. That's because I need something that's created very late in order to initialize it.

Specifically, I wish I had:

private lateinit val controlObj:SomeView

or:

private val controlObj:SomeView by lazy { view:View->view.findViewById(...)}

and then:

override fun onCreateView(....) {
    val view = inflate(....)


    controlObj = view.findViewById(...)

or in the 2nd case controlObj.initWith(view) or something like that:

return view

I cannot use by lazy because by lazy won't accept external parameters to be used when initialising. In this example - the containing view.

Of course I have lateinit var but it would be nice if I could make sure it becomes read only after setting and I could do it in one line.

Is there a pretty clean way to create a read only variable that initializes only once but only when some other variables are born? Any init once keyword? That after init the compiler knows it's immutable?

I am aware of the potential concurrency issues here but if I dare to access it before init, I surely deserve to be thrown.

Subduct answered 25/1, 2018 at 12:41 Comment(2)
How can we use Kotlin contracts to do this? I'm looking it up but I don't see how.Hiett
Sorry, @Hiett I think I saw something long ago... Been a year since, can't remember.Subduct
R
23

You can implement own delegate like this:

class InitOnceProperty<T> : ReadWriteProperty<Any, T> {

    private object EMPTY

    private var value: Any? = EMPTY

    override fun getValue(thisRef: Any, property: KProperty<*>): T {
        if (value == EMPTY) {
            throw IllegalStateException("Value isn't initialized")
        } else {
            return value as T
        }
    }

    override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
        if (this.value != EMPTY) {
            throw IllegalStateException("Value is initialized")
        }
        this.value = value
    }
}

After that you can use it as following:

inline fun <reified T> initOnce(): ReadWriteProperty<Any, T> = InitOnceProperty()

class Test {

     var property: String by initOnce()

     fun readValueFailure() {
         val data = property //Value isn't initialized, exception is thrown
     }

     fun writeValueTwice() {
         property = "Test1" 
         property = "Test2" //Exception is thrown, value already initalized
     }

     fun readWriteCorrect() {
         property = "Test" 
         val data1 = property
         val data2 = property //Exception isn't thrown, everything is correct
     }

}

In case when you try to access value before it is initialized you will get exception as well as when you try to reassign new value.

Rubicon answered 25/1, 2018 at 14:20 Comment(6)
Here variable is var not valKashakashden
Yep, and as said in title of question "or, alternatively, a var that can set once"Rubicon
"Specifically, I wish I had private lateinit val controlObj:SomeView or private val controlObj:SomeView by lazy { view:View->view.findViewById(...)}"Kashakashden
Yeah, looks like the "School solution". More or less, that's what I thought... No shortcut from that :-( Except, of course, for the easy case like view where you have getView evaluated on the lazy call.Subduct
You can use check to make the code a little cleaner. check(value != EMPTY) { "Value isn't initialized" } and check(this.value == EMPTY) { "Value is already initialized" }Kieffer
The problem with this solution is that the Kotlin compiler is unaware of the only-set-once guarantee, so for example it will not be able to apply smart casts to class members like it does with val, because it assumes the value may change at any time. It also can't apply common subexpression optimizations.Mcbroom
G
7

In this solution you implement a custom delegate and it becomes a separate property on your class. The delegate has a var inside, but the controlObj property has the guarantees you want.

class X {
    private val initOnce = InitOnce<View>()
    private val controlObj: View by initOnce

    fun readWithoutInit() {
        println(controlObj)
    }

    fun readWithInit() {
        initOnce.initWith(createView())
        println(controlObj)
    }

    fun doubleInit() {
        initOnce.initWith(createView())
        initOnce.initWith(createView())
        println(controlObj)
    }
}

fun createView(): View = TODO()

class InitOnce<T : Any> {

    private var value: T? = null

    fun initWith(value: T) {
        if (this.value != null) {
            throw IllegalStateException("Already initialized")
        }
        this.value = value
    }

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T =
            value ?: throw IllegalStateException("Not initialized")
}

BTW if you need thread safety, the solution is just slightly different:

class InitOnceThreadSafe<T : Any> {

    private val viewRef = AtomicReference<T>()

    fun initWith(value: T) {
        if (!viewRef.compareAndSet(null, value)) {
            throw IllegalStateException("Already initialized")
        }
    }

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T =
            viewRef.get() ?: throw IllegalStateException("Not initialized")
}
Glyconeogenesis answered 25/1, 2018 at 15:7 Comment(4)
One thing that can be improved - it is handling of null. Sometimes view can be absent and in such case the exception is thrown. In any other aspects it is the best solution.Rubicon
You think a different exception should be thrown instead of IllegalStateException? BTW i'm not sure i like this one better than your aproach with a guaranteed single-assign to var. This one has the burden of the exposed delegate.Glyconeogenesis
I am saying that findViewById can return null in case when view is absent in view hierarchy and it is ok. But since you do view ?: throw IllegatStateException for determining if value initialized, null cannot be stored in your delegate even if it is valid value.Rubicon
The whole API of InitOnce is based on the non-nullable type View so this presents a consistent story. Sure, if another use case asks for a nullable val, this could be adapted.Glyconeogenesis
E
5

You can use lazy. For example with TextView

    val text by lazy<TextView?>{view?.findViewById(R.id.text_view)}

where view is getView(). And after onCreateView() you can use text as read only variable

Erysipelas answered 25/1, 2018 at 13:55 Comment(3)
This is not what question is about. In your case, value of variable is determined statically.Kashakashden
Interesting, though... Solves some use cases. view is actually a method, getView(). It is evaluated by the time of invocation.Subduct
And - when doing that - beware of silly mistake #1: onCreateView(...) { myTextView.text = "Boo!" } will not work because when done from onCreateView, view as in getView() may still be null :-( Just reminding...Subduct
D
3

You can implement own delegate like this:

private val maps = WeakHashMap<Any, MutableMap<String, Any>>()

object LateVal {
    fun bindValue(any: Any, propertyName: String, value: Any) {
        val map = maps.getOrPut(any) { mutableMapOf<String, Any>() }

        if (map[propertyName] != null) {
            throw RuntimeException("Value is initialized")
        }

        map[propertyName] = value
    }

    fun <T> lateValDelegate(): MyProperty<T> {
        return MyProperty<T>(maps)
    }

    class MyProperty<T>(private val maps: WeakHashMap<Any, MutableMap<String, Any>>) : ReadOnlyProperty<Any?, T> {

        override fun getValue(thisRef: Any?, property: KProperty<*>): T {
            val ret = maps[thisRef]?.get(property.name)
            return (ret as? T) ?: throw RuntimeException("Value isn't initialized")
        }
    }
}

fun <T> lateValDelegate(): LateVal.MyProperty<T> {
    return LateVal.MyProperty<T>(maps)
}

fun Any.bindValue(propertyName: String, value: Any) {
    LateVal.bindValue(this, propertyName, value)
}

After that you can use it as following:

class Hat(val name: String = "casquette") {
    override fun toString(): String {
        return name
    }
}

class Human {
    private val hat by lateValDelegate<Hat>()

    fun setHat(h: Hat) {
        this.bindValue(::hat.name, h)
    }

    fun printHat() {
        println(hat)
    }

}

fun main(args: Array<String>) {
    val human = Human()
    human.setHat(Hat())
    human.printHat()
}

In case when you try to access value before it initialized you will get exception as well as when you try to reassign new value.

also,you can write DSL to make it readable.

object to

infix fun Any.assigned(t: to) = this

infix fun Any.property(property: KProperty<*>) = Pair<Any, KProperty<*>>(this, property)

infix fun Pair<Any, KProperty<*>>.of(any: Any) = LateVal.bindValue(any, this.second.name, this.first)

and then call it like this:

fun setHat(h: Hat) {
    h assigned to property ::hat of this
}
Decadence answered 10/12, 2018 at 7:29 Comment(1)
Update: the times they are achangin' ... contracts are a good read. Still experimental but you can do cool things with contractsSubduct
F
2

Safe delegation, Synchronized and helpfull messages

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

interface InitOnce<T> : ReadWriteProperty<Any?, T> {

    val isInitialized: Boolean

    val value: T

}

class SynchronizedInitOnceImpl<T> : InitOnce<T> {

    object UNINITIALIZED_VALUE

    private var name: String? = null

    @Volatile
    private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        @Suppress("UNCHECKED_CAST")
        get() {

            val _v = synchronized(this) { _value }

            if (_v !== UNINITIALIZED_VALUE) return _v as T
            else error("'$name' not initialized yet")

        }

    override val isInitialized: Boolean
        get() = _value !== UNINITIALIZED_VALUE

    override operator fun getValue(thisRef: Any?, property: KProperty<*>): T {

        if(name == null) name = property.name

        return value

    }

    override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {

        synchronized(this) {

            val _v = _value
            if (_v !== UNINITIALIZED_VALUE) error("'${property.name}' already initialized")
            else _value = value

        }

    }

}

fun <T> initOnce(): InitOnce<T> = SynchronizedInitOnceImpl()

Usage

var hello: String by initOnce()
Flareup answered 7/6, 2020 at 20:35 Comment(0)
D
1

You can implement own delegate like this:

class LateInitVal {
    private val map: MutableMap<String, Any> = mutableMapOf()

    fun initValue(property: KProperty<*>, value: Any) {
        if (map.containsKey(property.name)) throw IllegalStateException("Value is initialized")

        map[property.name] = value
    }

    fun <T> delegate(): ReadOnlyProperty<Any, T> = MyDelegate()

    private inner class MyDelegate<T> : ReadOnlyProperty<Any, T> {

        override fun getValue(thisRef: Any, property: KProperty<*>): T {
            val any = map[property.name]
            return any as? T ?: throw IllegalStateException("Value isn't initialized")
        }

    }
}

After that you can use it as following:

class LateInitValTest {
    @Test
    fun testLateInit() {
        val myClass = MyClass()

        myClass.init("hello", 100)

        assertEquals("hello", myClass.text)
        assertEquals(100, myClass.num)
    }
}

class MyClass {
    private val lateInitVal = LateInitVal()
    val text: String by lateInitVal.delegate<String>()
    val num: Int by lateInitVal.delegate<Int>()

    fun init(argStr: String, argNum: Int) {
        (::text) init argStr
        (::num) init argNum
    }

    private infix fun KProperty<*>.init(value: Any) {
        lateInitVal.initValue(this, value)
    }
}

In case when you try to access value before it initialized you will get exception as well as when you try to reassign new value.

Decadence answered 1/9, 2018 at 12:48 Comment(0)
V
1

you can do it like:

var thing: Int? = null
set(value) {
    if (field == null) field = value
    else throw RuntimeException("thing is already set")
}
Verada answered 21/6, 2022 at 6:27 Comment(0)
S
0

If you really want a variable to be set only once you can use a singleton pattern:

companion object {
    @Volatile private var INSTANCE: SomeViewSingleton? = null

    fun getInstance(context: Context): SomeViewSingleton =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: buildSomeViewSingleton(context).also { INSTANCE = it }
            }

    private fun buildSomeViewSingleton(context: Context) =
            SomeViewSingleton(context)
}

Then all you have to do is call getInstance(...) and you will always get the same object.

If you want to bind the lifetime of the object to the surrounding object just drop the companion object and put the initializer in your class.

Also the synchronized block takes care of the concurrency issues.

Strategist answered 25/1, 2018 at 14:6 Comment(2)
It's wrong. Variable is mutable, var instead of val. This is not what author wantedKashakashden
Indeed, the class object could modify the private variable. On the other hand this solution provides a way to have safe public and concurrent access from other classes. The only caveat is that you must be disciplined not to touch the private var, but rather use the getInstancemethod. I agree, that is not 100% what was asked, but close.Strategist
A
0

For Activity it is OK to do follow:

private val textView: TextView by lazy { findViewById<TextView>(R.id.textView) }

For Fragment it is not makes sense to create final variable with any View type, because you will lost connection with that view after onDestroyView.

P.S. Use Kotlin synthetic properties to access views in Activity and Fragment.

Amphibolous answered 25/1, 2018 at 14:55 Comment(1)
Better not, because every findViewById will DFS from the root view. It costs more CPU. I would use the lowest view in the tree from which I can conveniently search views.Subduct
P
0

Just use a HashMap with initial capacity of 1 (ConcurrentHashMap if threading is a concern) with computeIfAbsent (which will only initialize the value for a given key once)

val fooByBar = HashMap<Bar, Foo>(1)

fun getFoo(bar: Bar) = fooByBar.computeIfAbsent(bar) {
    fooFactory.create(bar)
}

try {
  bazes.forEach { baz ->
     val foo = getFoo(baz.bar) // assuming bar is same for all bazes, if not then it's still fine
     foo.quux()
  }
} catch (ex: Exception) {
    logger.error(ex.message, ex)
    return@whatever
} finally {
    fooByBar.forEach { (_, foo) ->
       foo.close() // if Foo : AutoCloseable
    }
}
Pasteurization answered 26/8, 2021 at 14:10 Comment(0)
U
0

Another pattern to consider (inspired just a little by Koin )

inline fun <reified V: Any> WeakHashMap<KClass<Any>, Any>.get(): V {
    val key = V::class as KClass<Any>
    val value: V? = this[key] as V?
    assert(value is V)
    return value!!
}

inline fun <reified V: Any> WeakHashMap<KClass<Any>, Any>.put(value: V) {
    val key = V::class as KClass<Any>
    this[key] = value
}


class A(val b: B, val c: C)
class B(val c: C, injectA: () -> A) {
    val a: A by lazy { injectA() }
}
class C(injectA: () -> A, injectB: () -> B) {
    val a by lazy { injectA() }
    val b by lazy { injectB() }
}


val scope = WeakHashMap<KClass<Any>, Any>()
val c = C({ scope.get<A>() }, { scope.get<B>() })
scope.put(c)
val b = B(c, { scope.get<A>() })
scope.put(b)
val a = A(b, c)
scope.put(a)

assert(a.b === b)
assert(a.c === c)
assert(b.c === c)
assert(b.a === a)
assert(c.a === a)
assert(c.b === b)
Upanishad answered 19/12, 2023 at 14:19 Comment(0)
K
-2

I believe there is no such thing "init once". Variables are either final or not. lateinit variables are not final. Because, well, you reassign it some other value after initialization phase.

If someone finds the solution, I'm starring this question

Kashakashden answered 25/1, 2018 at 13:18 Comment(2)
This is not an answer, but an opinion.Strategist
I'm sorry if you understood me wrong, I did not want to offend you, rather I wanted to tell you why your answer is not helpful (It comes off as being based on belief, rather than knowledge (which I can only assume you have)). I understand that you had the best intentions. In my opinion your answer is not an 'answer', but should rather be a comment on the question.Strategist

© 2022 - 2025 — McMap. All rights reserved.