15

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?

s1m0nw1
  • 76,759
  • 17
  • 167
  • 196
Nikolay Kulachenko
  • 4,604
  • 4
  • 31
  • 37
  • 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? – dkarmazi Dec 22 '17 at 22:19
  • 1
    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 encapsulation – Boris Treukhov Dec 22 '17 at 22:26
  • @dkarmazi 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. – Nikolay Kulachenko Dec 24 '17 at 09:59
  • Possible duplicate of [Kotlin lazy properties and values reset: a resettable lazy delegate](https://stackoverflow.com/questions/35752575/kotlin-lazy-properties-and-values-reset-a-resettable-lazy-delegate) – breandan Oct 26 '19 at 17:16

4 Answers4

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
        }
    }
}
arberg
  • 4,148
  • 4
  • 31
  • 39
s1m0nw1
  • 76,759
  • 17
  • 167
  • 196
  • btw lazy() at least implements infamous double checked locking - if you open Lazy.kt source code you'll see it's pretty sophisticated – Boris Treukhov Dec 22 '17 at 22:27
  • tbh I thought it will be a reusable custom delegate :) – Boris Treukhov Dec 22 '17 at 22:33
  • 1
    there you go, not as sophisticated as lazy though ;-) – s1m0nw1 Dec 22 '17 at 22:57
  • 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) – Nikolay Kulachenko Dec 24 '17 at 10:07
  • You're right, sorry I missed that part, fixed with `return if (prop == UNINITIALIZED_VALUE) initializer().also { prop = it } else prop as T ` – s1m0nw1 Dec 24 '17 at 10:14
  • Is it also possible to call `fun isInitialized(): Boolean` like here: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-lazy/index.html ? I couldn't find a way to do so... – Paul Spiesberger Jul 20 '18 at 14:05
  • 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. – talosdev Oct 11 '19 at 09:33
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
Kobato
  • 726
  • 1
  • 7
  • 15
  • why synchronized ? – Renetik Jul 04 '22 at 17:54
  • @Renetik 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 threads – Kobato Jul 04 '22 at 21:08
  • 1
    sure I realized later that original kotlin lazy implementation is also synchronized so ti makes sense... – Renetik Jul 04 '22 at 21:20
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)

Nolan Amy
  • 10,785
  • 5
  • 34
  • 45
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
        }
}
Renetik
  • 5,887
  • 1
  • 47
  • 66