article banner (priority)

Map as a property delegate

This is a chapter from the book Advanced Kotlin. You can find it on LeanPub.

The last delegate from Kotlin standard library is Map with keys of type String. When we use it as a delegate, and we ask for property value, the result is the value associated with this property name.

fun main() { val map: Map<String, Any> = mapOf( "name" to "Marcin", "kotlinProgrammer" to true ) val name: String by map val kotlinProgrammer: Boolean by map println(name) // Marcin println(kotlinProgrammer) // true }

How can Map be a delegate? To be able to use an object as a read-only property delegate, this object must have a getValue function. For Map, it is defined as an extension function in Kotlin stdlib.

operator fun <V, V1 : V> Map<String, V>.getValue( thisRef: Any?, property: KProperty<*> ): V1 { val key = property.name val value = get(key) if (value == null && !containsKey(key)) { throw NoSuchElementException( "Key ${property.name} is missing in the map." ) } else { return value as V1 } }

So what are use-cases for using Map as a delegate? In most applications, you should not need it. However, some API might force you to treat objects as maps with some expected properties, and some properties that are not expected. While using one API, I got informed by its creators, that "This endpoint will return an object representing user, with properties id, displayName, and also some other properties, and on the profile page you need to iterate over all properties, also not known in advance, and display appropriate view for each of them". For such a requirement, we need to represent an object using Map, and to more easily use properties we know we can expect, we can use this map as a delegate.

class User(val map: Map<String, Any>) { val id: Long by map val name: String by map } fun main() { val user = User( mapOf<String, Any>( "id" to 1234L, "name" to "Marcin" ) ) println(user.name) // Marcin println(user.id) // 1234 println(user.map) // {id=1234, name=Marcin} }

Map can only be used for read-only properties, because it is a read-only interface. For read-write properties, use MutableMap. Any delegated property change is a change in the map it delegates to, also any change in this map leads to a different property value. Delegating to the MutableMap map is like accessing the shared state of a read/write data source.

class User(val map: MutableMap<String, Any>) { var id: Long by map var name: String by map } fun main() { val user = User( mutableMapOf( "id" to 123L, "name" to "Alek", ) ) println(user.name) // Alek println(user.id) // 123 user.name = "Bolek" println(user.name) // Bolek println(user.map) // {id=123, name=Bolek} user.map["id"] = 456 println(user.id) // 456 println(user.map) // {id=456, name=Bolek} }

Review of how variables work

A few years ago, the following puzzler was circulating across Kotlin community. A few people asked me to explain it before even I included it in my Kotlin workshop.

class Population(var cities: Map<String, Int>) { val sanFrancisco by cities val tallinn by cities val kotlin by cities } val population = Population( mapOf( "sanFrancisco" to 864_816, "tallinn" to 413_782, "kotlin" to 43_005 ) ) fun main(args: Array<String>) { // Years has passed, // now we all live on Mars population.cities = emptyMap() println(population.sanFrancisco) println(population.tallinn) println(population.kotlin) }

Before going any further, try to guess the answer.

The behavior presented by the above puzzler might be counterintuitive, but it is certainly correct. I will present the answer after I give a proper step-by-step rationale.

Let's start with a simpler example. Take a look at the following code.

fun main() { var a = 10 var b = a a = 20 println(b) }

What will be printed? The answer is 10. It is because variables are always assigned to values, never other variables. First, a is assigned to 10, then b is assigned to 10, then a changes and assigns to 20. This does not change that b is assigned to 10.

This picture gets more complicated when we introduce mutable objects. Take a look at the following snippet.

fun main() { val user1 = object { var name: String = "Rafał" } val user2 = user1 user1.name = "Bartek" println(user2.name) }

What will be printed? The answer is "Bartek". Here both user1 and user2 reference the same object, and then this object changes internally.

The situation would be different if we would change what user1 references instead of changing the value of the name.

interface Nameable { val name: String } fun main() { var user1: Namable = object : Nameable { override var name: String = "Rafał" } val user2 = user1 user1 = object : Nameable { override var name: String = "Bartek" } println(user2.name) }

What is the answer? It is "Rafał" now.

This is especially confusing if we compare mutable lists with read-only lists referenced with var. Especially since both can be changed with the += sign. First, take a look at the following snippet:

fun main() { var list1 = listOf(1, 2, 3) var list2 = list1 list1 += 4 println(list2) }

What will be printed? The answer is [1, 2, 3]. It has to be this way because list1 references a read-only list. This means that list1 += 4 in this case means list1 = list1 + 4, and so a new list object is returned. Now a mutable list:

fun main() { val list1 = mutableListOf(1, 2, 3) val list2 = list1 list1 += 4 println(list2) }

What will be printed? The answer is [1, 2, 3, 4]. It is because list1 references a mutable list. This means that list1 += 4 in this case means list1.plusAssign(4) that is list1.add(4), and the same list object is returned. Now consider using Map as a delegate:

fun main() { var map = mapOf("a" to 10) val a by map map = mapOf("a" to 20) println(a) }

Can you see that the answer should be 10? On the other hand, if the map would be mutable, the answer would be different:

fun main() { val mmap = mutableMapOf("a" to 10) val a by mmap mmap["a"] = 20 println(a) }

Can you see that the answer should be 20? This is consistent with the behavior of the other variables. It is also consistent with what properties are compiled to.

var map = mapOf("a" to 10) // val a by map // is compiled to val `a$delegate` = map val a: Int get() = `a$delegate`.getValue(null, ::a) val mmap = mutableMapOf("b" to 10) // val b by mmap // is compiled to val `b$delegate` = mmap val b: Int get() = `b$delegate`.getValue(null, ::b) fun main() { map = mapOf("a" to 20) println(a) // 10 mmap["b"] = 20 println(b) // 20 }

Finally, let's get back to our puzzler again. I hope you can see now that changing cities property should not influence the value of sanFrancisco, tallinn, or kotlin. In Kotlin, we delegate to a delegate, not to a property, just like we assign property to a value, not another property.

class Population(var cities: Map<String, Int>) { val sanFrancisco by cities val tallinn by cities val kotlin by cities } val population = Population( mapOf( "sanFrancisco" to 864_816, "tallinn" to 413_782, "kotlin" to 43_005 ) ) fun main(args: Array<String>) { // Years has passed, // now we all live on Mars population.cities = emptyMap() println(population.sanFrancisco) println(population.tallinn) println(population.kotlin) }

To clear populations, the cities map would have to be mutable and using population.cities.clear() would cause population.sanFrancisco to fail.

Summary

In this chapter, you've learned how property delegation works and how to define custom property delegates ourselves. You also learned about the most important property delegates from the Kotlin standard library: Delegates.notNull, lazy, Delegates.observable, Delegates.vetoable, Map<String, T> and MutableMap<String, T>. I hope you will find this knowledge useful in your programming practice.