Lazy property delegate
This is a chapter from the book Advanced Kotlin. You can find it on LeanPub.
The most popular property delegate is
lazy. It implements the lazy property pattern, so it postpones read-only value initialization until the moment when this value is needed for the first time. This is what an example
lazy usage looks like and what we would need to implement if we wouldn't have this delegate1:
The lazy delegate is often used as a performance optimization for objects that are expensive to calculate. Let's start with an abstract example. Consider class
A, which composes classes
D, each heavy to initialize. This makes the instance of
A really heavy to initialize because its initialization includes the initialization of multiple heavy objects.
We can make
A initialization lighter by making
d properties lazy. Thanks to that, their values initialization is postponed until their first use, and if those properties are never used, we will never have associated class instances initialized. Such an operation improves class creation time, which benefits application startup time and tests execution time.
To make this example more practical,
A might be
FlashcardsParser used to parse files defined in a language we invented, and
D might be some complex regex parsers, which are heavy to initialize.
Regex is a really useful notation when we need to process text. However, regex definitions can be really complex, and their parsing is a heavy operation. This reminds me of another example I have famously shown in the Effective Kotlin book. Consider the function where we use a regex to determine whether a string contains a valid IP address:
The problem with this function is that the
Regex object needs to be created every time we use it. This is a serious disadvantage since regex pattern compilation is a complex operation. This is why this function is not suitable for repeated use in performance-constrained parts of our code. However, we can improve it by lifting the regex up to the top level:
The problem now is that this regex is initialized whenever we initialize this file for the first time, and if this file contains more elements (like other functions or properties), this regex might slow down its usage for no good reason. This is why it is better to make the
IS_VALID_IP_REGEX property lazy.
This technique4 can be used on a variety of occasions. Consider a data class with a computed property3 that is not trivial to calculate. Like our
User, with the
fullDisplay property, which is calculated on other properties, but includes some significant logic, as it depends on multiple properties and project configurations. If we define the
fullDisplay as a regular property, it will be calculated whenever a new instance of
User is created, which might be an unnecessary cost.
If we define
fullDisplay property using getter, it will be re-calculated whenever it is used, what also might be an unnecessary cost.
fullDisplay property as lazy is a sweet spot for properties that:
- are read-only (
lazycan only be used for
- are non-trivial to calculate (otherwise, using
lazyis not worth the effort),
- might not be used for all instances (otherwise use the regular property),
- might be used more than once by one instance (otherwise, define property with getter).
When we consider
lazy delegate as a performance optimization, we should also consider what thread safety mode we want it to use. It can be specified in an additional function argument
mode accepting the enum
LazyThreadSafetyMode. There are the following options:
SYNCHRONIZEDis the default and the safest option that uses locks to ensure that only a single thread can initialize this delegate instance. This option is also the slowest, as synchronization mechanisms introduce some performance costs.
PUBLICATIONmeans that the initializer function can be called several times on concurrent access to an uninitialized delegate instance value, but only the first returned value will be used as the value of this delegate instance. If a delegate is used by only a single thread, this option will be slightly faster than
SYNCHRONIZED, but when used by multiple threads, we need to cover the possible cost of the same value recalculation before the value is initialized.
NONEis the fastest option, which uses no locks to synchronize access to the delegate instance value, so if the instance is accessed from multiple threads, its behavior is undefined. This mode should not be used unless this instance is guaranteed never to be initialized from more than one thread.
In all the previous examples, we used
lazy as a performance optimization, but this is not the only reason. With that, I would like to share a story from my early days of using Kotlin on Android. It was around 2015. Kotlin was still before the stable release, and the Kotlin community was inventing ways how its features can be used to help us with everyday tasks. You see, in Android, we have a concept of Activity, which is like a window that defines its view, traditionally using XML files. Then the class representing Activity often modified this view by programmatically changing texts or some of its properties. To change a particular view element, we need to reference it, but we cannot reference it before the view is set with the
setContentView function. That is why back then, it was a standard to define references to view elements as lateinit properties and associate proper view elements to them straight after setting content view.
This pattern was used in nearly all Android applications (and I can still see it nowadays in some projects), even though it is far from perfect. We need to define multiple lateinit properties. They are all read-write, even though they are initialized only once. Property definition and assignment are separated. The property name repeats. If a property is not used, it will not be marked by IDE because the assignment is considered a usage. Overall, there is a lot of space for improvement. The solution turned out to be extremely simple: We can define property initialization next to its definition if we make it lazy.
Thanks to the
lazy delegate, finding the view by id will be postponed until the property is used for the first time, and we can assume it will happen after the content view is set. We made such an assumption in the previous solution, so why not make it in this one as well? We also have some important improvements: Property is read-only, initialization and definition are kept together, and unused properties will be marked. We also have performance benefits: View reference that is never used will never be associated with the view element, and
findViewById execution can be expensive.
What is more, we can push this pattern further and extract a function that will both define a lazy delegate and find view by id. In the project I co-created, we named it
bindView, and it helped us make our view references really clear, consistent, and readable.
We started using this pattern to bind other references as well, like strings or colors. But my favorite part is binding Activity arguments. I still use this in some of my projects. This is really convenient to have all activity properties defined together with their key at the top of this activity definition.
In the backend application, you can also find lazy delegates used for initializing properties that should not be initialized during project setup, but when they are needed for the first time. A simple example is a connection to a local database.
As you can see,
lazy is a powerful delegate that is mainly used for performance optimization but can also be used to simplify our code. I would like to finish this section with a small puzzler. Consider the following code2:
What will be printed? Try to answer this question before reading any further.
To understand the answer, we need to consider three facts:
- Delegate exceptions are propagated out of accessors.
- Lazy delegate first tries to return previously calculated value, and if there is no value stored, it uses a lambda expression to calculate it. If the calculated process is disturbed with an exception, no value is stored, and when we ask for the lazy value the next time, processing starts again.
- Kotlin's lambda expressions are automatically capturing variable references, so when we use
xinside the lambda expression, every time we use it, we will receive the current value of
x, so when we call the lambda expression for the second time,
xis 1, so the result should be 1.
So the answer to this puzzler is "1".
In the next part of this series, I will show you observable and vetoable, that are other amazing property delegates. So stay tuned.
lazy implementation is more complicated, as it has better synchronization securing it for concurrent use.
I first heard about this puzzler from Anton Keks' presentation, and it can be found in his repository, so I assume he is the author.
By computed property, I mean a property whose value is determined by other properties, so can always be recalculated.
Also known as memoization.