Solution: Lateinit delegate
The naive solution to this problem would use null
as a marker for a value that has not been initialized, but this solution would not work correctly for nullable types.
// Solution that does not work correctly for nullable types
class Lateinit<T>: ReadWriteProperty<Any?, T> {
var value: T? = null
override fun getValue(
thisRef: Any?,
prop: KProperty<*>
): T =
value ?: error(
"Uninitialized lateinit property ${prop.name}"
)
override fun setValue(
thisRef: Any?,
prop: KProperty<*>,
value: T
) {
this.value = value
}
}
You should replace "..." with "Uninitialized lateinit".
Correct solutions require a different way of keeping information about values that have not been initialized. It can be another property, a special flag, or an object.
class Lateinit<T>: ReadWriteProperty<Any?, T> {
var value: T? = null
var isInitialized = false
override fun getValue(
thisRef: Any?,
prop: KProperty<*>
): T {
if (!isInitialized) {
error("Uninitialized lateinit property ${prop.name}")
}
return value as T
}
override fun setValue(
thisRef: Any?,
prop: KProperty<*>,
value: T
) {
this.value = value
this.isInitialized = true
}
}
This solution is not thread safe. If two threads try to set the value at the same time, one thread might overwrite the value set by the other thread. To make it thread safe, we can use a synchronized
block.
class Lateinit<T>: ReadWriteProperty<Any?, T> {
var value: Any? = NOT_INITIALIZED
override fun getValue(
thisRef: Any?,
prop: KProperty<*>
): T {
if (value == NOT_INITIALIZED) {
error("Uninitialized lateinit property ${prop.name}")
}
return value as T
}
override fun setValue(
thisRef: Any?,
prop: KProperty<*>,
value: T
) {
this.value = value
}
companion object {
val NOT_INITIALIZED = Any()
}
}
class Lateinit<T> : ReadWriteProperty<Any?, T> {
var value: ValueHolder<T> = NotInitialized
override fun getValue(
thisRef: Any?,
prop: KProperty<*>
): T = when (val v = value) {
NotInitialized ->
error("Uninitialized lateinit property ${prop.name}")
is Value -> v.value
}
override fun setValue(
thisRef: Any?,
prop: KProperty<*>,
value: T
) {
this.value = Value(value)
}
sealed interface ValueHolder<out T>
class Value<T>(val value: T) : ValueHolder<T>
object NotInitialized : ValueHolder<Nothing>
}
import org.junit.Test
import kotlin.properties.Delegates
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlin.test.assertEquals
import kotlin.test.assertIs
class Lateinit<T>: ReadWriteProperty<Any?, T> {
var value: T? = null
var isInitialized = false
override fun getValue(
thisRef: Any?,
prop: KProperty<*>
): T {
if (!isInitialized) {
error("Uninitialized lateinit property ${prop.name}")
}
return value as T
}
override fun setValue(
thisRef: Any?,
prop: KProperty<*>,
value: T
) {
this.value = value
this.isInitialized = true
}
}
// Implement Lateinit delegate here
// Easy: Uncomment the first 2 tests and implement Lateinit so it works for Int type
// Medium: Uncomment the first 3 tests and implement Lateinit so it works for all non-nullable types
// Hard: Uncomment all tests and implement Lateinit so it works for all types
class LateinitTest {
@Test
fun `Throws exception if accessed before initialization`() {
var value: Int by Lateinit()
val res = runCatching {
println(value)
}
val exception = assertIs<IllegalStateException>(res.exceptionOrNull())
assertEquals("Uninitialized lateinit property value", exception.message)
var value2: Int by Lateinit()
val res2 = runCatching {
println(value2)
}
val exception2 = assertIs<IllegalStateException>(res2.exceptionOrNull())
assertEquals("Uninitialized lateinit property value2", exception2.message)
}
@Test
fun `Behaves like a normal variable for Int`() {
var value: Int by Lateinit()
value = 10
assertEquals(10, value)
value = 20
assertEquals(20, value)
}
@Test
fun `Behaves like a normal variable for String`() {
var value: String by Lateinit()
value = "AAA"
assertEquals("AAA", value)
value = "BBB"
assertEquals("BBB", value)
}
@Test
fun `Behaves like a normal variable for nullable String`() {
var value: String? by Lateinit()
value = "AAA"
assertEquals("AAA", value)
value = null
assertEquals(null, value)
value = "BBB"
assertEquals("BBB", value)
}
}
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.