article banner

Map as a property delegate

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

The last delegate from Kotlin standard library is Map, which has keys of type String. When we use it as a delegate and we ask for a 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 some use cases for using Map as a delegate? In most applications, you should not need it. However, you might be forced by an API to treat objects as maps that have some expected keys and some that might be added dynamically in the future. I mean situations like "This endpoint will return an object representing a user, with properties id, displayName, etc., and on the profile page you need to iterate over all these properties, including those that are not known in advance, and display an appropriate view for each of them". For such a requirement, we need to represent an object using Map, then we can use this map as a delegate in order to more easily use properties we know we can expect.

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, and any change in this map leads to a different property value. Delegating to a 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 puzzle was circulating in the Kotlin community. A few people asked me to explain it before I even 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 this puzzle 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 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 is assigned to 20. This does not change the fact 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 changed what user1 references instead of changing the value of 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ł".

This is particularly 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, let's take a look at the following snippet:

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] because list1 references a mutable list. This means that list1 += 4 in this case means list1.plusAssign(4), i.e., 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 were 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 and 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 puzzle again. I hope you can see now that changing the 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 a 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 the 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. You’ve 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.