Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 305
Maneki Neko Avatar asked Jan 25 '18 12:01

Maneki Neko


People also ask

Can we use Lateinit with Val?

You cannot use val for lateinit variable as it will be initialized later on.

How do I declare Lateinit VAR in Kotlin?

NOTE: To use a lateinit variable, your variable should use var and NOT val . Lateinit is allowed for non-primitive data types only and the variable can't be of null type. Also, lateinit variable can be declared either inside the class or it can be a top-level property.

How do you use Lateinit var Kotlin?

In order to create a "lateInit" variable, we just need to add the keyword "lateInit" as an access modifier of that variable. Following are a set of conditions that need to be followed in order to use "lateInit" in Kotlin. Use "lateInit" with a mutable variable. That means, we need to use "var" keyword with "lateInit".

What is var and val in Kotlin?

AndroidMobile DevelopmentApps/ApplicationsKotlin. In Kotlin, we can declare a variable using two different keywords: one is var and the other one is val.


5 Answers

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.

like image 61
hluhovskyi Avatar answered Oct 03 '22 02:10

hluhovskyi


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")
}
like image 31
Marko Topolnik Avatar answered Oct 03 '22 04:10

Marko Topolnik


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

like image 28
Stanislav Bondar Avatar answered Oct 03 '22 04:10

Stanislav Bondar


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.

like image 41
sunhang Avatar answered Oct 03 '22 03:10

sunhang


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()
like image 33
Alireza Ghasemi Avatar answered Oct 03 '22 02:10

Alireza Ghasemi