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.
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.
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.
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.
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.
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.
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.
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
.
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:
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:
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:
Can you see that the answer should be 10
? On the other hand, if the map would be mutable, the answer would be different:
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.
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.
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.