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.
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 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.
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.
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.
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.
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.
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
.
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:
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:
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:
Can you see that the answer should be 10
? On the other hand, if the map were 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 and with what properties are compiled to.
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.
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.