Kotlin var lazy init
Asked Answered
R

4

17

I want to do a lazy initialization for a var property. Since by lazy is restricted only to val properties I have to write something like this:

    private var currentContextProvider: ContextProvider? = null
        get() {
            if (field == null) {
                field = DefaultContextProvider()
            }
            return field
        }

And now I have to deal with those pointless nullability-aware calls: currentContextProvider?.getContext() or alternatively currentContextProvider!!.getContext()

Am I doing something wrong?

Roselleroselyn answered 22/12, 2017 at 21:59 Comment(4)
it sounds like you're interested in two things happening: nullability and initialization. Does your variable have to be nullable? Do you want to initialize this item once and only once? Also, why do you want var and not val specifically?Inkerman
I'm sure someone will post a code on how to implement a custom delegate but I can't imagine any real life situation where you need to make a lazy property modifiable without compromising encapsulationIncongruous
@Inkerman I create a strategy where the default one is DefaultContextProvider. That's why I use var instead of val so I can change it in runtime. I also want to provide a stub strategy (not to mock the whole class) and test all the things. My test dependencies distinguish from the prod ones so it throws an error when it reaches DefaultContextProvider(). That's the reason of a lazy init. I set the stub strategy in onSetup before any call to this class and it goes ok except I have to deal with nullability-aware issues.Roselleroselyn
Possible duplicate of Kotlin lazy properties and values reset: a resettable lazy delegateCadre
R
18

Instead of making it nullable, you can decide to initialise it with some default value, which on first access will be replaced with the lazily calculated value:

private val noInit = "noinit"
var currentContextProvider: String = noInit
        get() = if (field == noInit) {
            synchronized(this) {
                return if (field == noInit) "lazyinit" else field
            }
        } else field

(I've replaced the ContextProvider with String)

Custom Delegate

The following implements a custom delegate reusing the former solution. It can be used just like lazy() by defining var currentContextProvider: ContextProvider by LazyMutable { DefaultContextProvider() }

class LazyMutable<T>(val initializer: () -> T) : ReadWriteProperty<Any?, T> {
    private object UNINITIALIZED_VALUE
    private var prop: Any? = UNINITIALIZED_VALUE

    @Suppress("UNCHECKED_CAST")
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return if (prop == UNINITIALIZED_VALUE) {
            synchronized(this) {
               return if (prop == UNINITIALIZED_VALUE) initializer().also { prop = it } else prop as T
            }
        } else prop as T
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        synchronized(this) {
            prop = value
        }
    }
}
Resurge answered 22/12, 2017 at 22:25 Comment(7)
btw lazy() at least implements infamous double checked locking - if you open Lazy.kt source code you'll see it's pretty sophisticatedIncongruous
tbh I thought it will be a reusable custom delegate :)Incongruous
there you go, not as sophisticated as lazy though ;-)Resurge
Shouldn't we save the value in a backing field here: return if (prop == UNINITIALIZED_VALUE) initializer() else prop as T but not just give it away? Otherwise it runs initializer() every call (until we explicitly set the value)Roselleroselyn
You're right, sorry I missed that part, fixed with return if (prop == UNINITIALIZED_VALUE) initializer().also { prop = it } else prop as T Resurge
Is it also possible to call fun isInitialized(): Boolean like here: kotlinlang.org/api/latest/jvm/stdlib/kotlin/-lazy/index.html ? I couldn't find a way to do so...Slumberland
Note that the inline solution (the first code block - without the delegate) would also need a backing property if you use an initializer, to avoid having the initializer be called each time the property is accessed.Akkad
B
1

I needed a lazy delegated property that initializes and caches when you get the property, but allows you to set it to null to remove that cached result (and re-initialize() it when you get it again).

Thanks to the above answer for the code so I could tweak it.

@Suppress("ClassName")
class lazyNullCacheable<T>(val initializer: () -> T) : ReadWriteProperty<Any?, T> {
    private object UNINITIALIZED_VALUE
    private var prop: Any? = UNINITIALIZED_VALUE

    @Suppress("UNCHECKED_CAST")
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return if (prop == UNINITIALIZED_VALUE || prop == null) {
            synchronized(this) {
                initializer().also { prop = it }
            }
        } else prop as T
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        synchronized(this) {
            prop = value
        }
    }
}

Usage :

var prop: String? by lazyNullCacheable {
    "string"
}

prop // get prop
prop = null // when you're done using it and want to recalculate / cache it next time
prop // get prop, it will initialize() and cache again

Functionally equivalent to doing something like this (but this is uglier imho)

var _prop: Type? = null
val prop: Type
    get() = _prop ?: run {
        _prop = Type()
        _prop!!
    }

prop // get result
_prop = null // clear cache
prop // next get will recalculate it
Biceps answered 26/8, 2021 at 20:48 Comment(4)
why synchronized ?Vaduz
@Vaduz This is just a copy of the accepted answer, with some small tweaking done to make it work slightly differently (calculate on get, clear on null, and recalculate on get). I'm not sure why they used synchronized, but I imagine it's important if you want to use this from different threadsBiceps
sure I realized later that original kotlin lazy implementation is also synchronized so ti makes sense...Vaduz
I thin you might want to reconsider such a design because it violates principle of least surprise - en.wikipedia.org/wiki/Principle_of_least_astonishment. Setting prop = null to null, and not getting null back in next statement will surprise the API user.Circumstantiate
N
0
class LazyMutable<T>(
    val initializer: () -> T,
) : ReadWriteProperty<Any?, T> {
    private val lazyValue by lazy { initializer() }
    private var newValue: T? = null

    override fun getValue(thisRef: Any?, property: KProperty<*>) =
        newValue ?: lazyValue

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

usage:

var foo by LazyMutable { "initial value" }

(doesn't support nullable foo)

Nibelungenlied answered 4/10, 2021 at 5:25 Comment(0)
V
0

Test:

class LazyVarTest {

    @Test
    fun testLazyVar() {
        var testVar: String by lazyVar { "initial" }
        assertEquals("initial", testVar)
        testVar = "test"
        assertEquals("test", testVar)
    }

    @Test
    fun testNullableLazyVar() {
        var testVar: String? by lazyVar { "initial" }
        assertEquals("initial", testVar)
        testVar = "test"
        assertEquals("test", testVar)
        testVar = null
        assertEquals(null, testVar)
    }
}

Implementation:

fun <T> lazyVar(initializer: () -> T) = LazyVar(initializer)

class LazyVar<T>(initializer: () -> T) : ReadWriteProperty<Any?, T> {
    private object initialValue

    var isSet = false
    private val lazyValue by lazy { initializer() }
    private var value: Any? = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): T =
        synchronized(this) {
            if (!isSet) return lazyValue
            return value as T
        }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) =
        synchronized(this) {
            this.value = value
            isSet = true
        }
}
Vaduz answered 4/7, 2022 at 21:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.