In Kotlin, delegation extends beyond classes to properties. In contrast to conventional properties that are directly backed by corresponding fields, delegated properties assign the responsibilities of getting and setting to a separate block of code. This allows for delegated functionality to be abstracted out and shared between multiple similar properties, enabling us to define common properties once and reuse them in multiple instances, thereby enhancing efficiency and maintainability.
Delegating properties: example
Consider a class named Example with two properties of type String: firstProp and secondProp. If we want these properties to have identical formatting rules, we could implement a setter function for each property:
class Example {
var firstProp: String = ""
set(value) {
// Remove all vowels and convert the remaining string to uppercase
field = value.replace(Regex("[aeiouAEIOU]"), "").uppercase()
}
var secondProp: String = ""
set(value) {
// Remove all vowels and convert the remaining string to uppercase
field = value.replace(Regex("[aeiouAEIOU]"), "").uppercase()
}
}
In this example, we define a set function for both properties to remove all vowels from the input value and convert the remaining string to uppercase. However, this approach leads to code repetition, making our code harder to test and maintain. A more efficient way to write this code is by delegating the setter function to a delegate:
class Example {
var firstProp: String by Formatter
var secondProp: String by Formatter
}
Delegated properties are declared by specifying the property and the delegate it uses. The by keyword indicates that the property is controlled by the provided delegate instead of its own field. The syntax is: val/var <property name>: <Type> by <delegate>. Let's see next how we can implement such a delegate.
Implementing the delegate
Property delegates need to always provide a getValue() function (and setValue() for vars). In our previous example, Formatter is the delegate controlling the getter and setter of the properties firstProp and secondProp. Here's how we implement it:
import kotlin.reflect.KProperty
class Formatter {
private var value: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value.replace(Regex("[aeiouAEIOU]"), "").uppercase()
}
}
As you can see, creating a custom property delegate in Kotlin involves defining a class that provides the getValue() and setValue() methods, which perform getting and setting operations for the property, respectively.
To better understand the parameters of each function, let's take the following example:
import kotlin.reflect.KProperty
class AnotherExample {
val stringProp: String by Delegate()
fun foo(): String {
return ""
}
}
class Delegate {
private var curValue = ""
operator fun getValue(thisRef: AnotherExample, property: KProperty<*>): String {
println(thisRef.stringProp + thisRef.foo()) // thisRef allows us to access any member of the class: AnotherExample
return curValue
}
}
In this example, we want to delegate a read-only property (val). To do that, we should provide a function getValue(), which is marked with the operator keyword, returns the same type as the delegating property (stringProp), and has the following parameters:
-
thisRef: it must be the same type as, or a supertype of, the class owning the delegating property. It represents the object that contains the delegated property and can be used to access other members of the object, such as other properties or functions. -
property: it must be of typeKProperty<*>and can be used to access the metadata of the delegated property. For example, if we want to print the name of the delegating property, we should write:
println(property.name) // stringProp
kotlin.reflect.KProperty in order to use the KProperty class in your code.Now, what happens if we change the property stringProp from val to var? In that case, we must provide an additional function setValue(), like this:
import kotlin.reflect.KProperty
class AnotherExample {
var stringProp: String by Delegate()
...
}
class Delegate {
private var curValue = ""
operator fun getValue(...): String { ... }
operator fun setValue(thisRef: AnotherExample, property: KProperty<*>, value: String) {
println("The new value of ${property.name} is: $value")
}
}
Here we provided a second function setValue(), which is marked with the operator keyword and has an additional parameter value, which must be of the same type as the delegating property.
Anonymous delegates
Kotlin allows you to create delegates as anonymous objects without the need for new classes. Simply use the ReadOnlyProperty and ReadWriteProperty interfaces from the Kotlin standard library. Just make sure to import the right package:
import kotlin.properties.ReadOnlyProperty // To use ReadOnlyProperty
import kotlin.properties.ReadWriteProperty // To use ReadWriteProperty
These interfaces provide the necessary methods:
getValue()is declared inReadOnlyProperty.- while
ReadWritePropertyextends it and addssetValue().
This means you can pass a ReadWriteProperty wherever ReadOnlyProperty is expected. Here's an example:
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
fun anonymousDelegate() = object : ReadWriteProperty<Any?, String> {
var curValue = ""
override fun getValue(thisRef: Any?, property: KProperty<*>) {
return curValue
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
curValue = value
println("The new value of ${property.name} is: $value")
}
}
fun main() {
val readOnlyString: String by anonymousDelegate()
var readWriteString: String by anonymousDelegate()
readWriteString = "Hello!" // The new value of readWriteString is: Hello!
}
In this example, we define a function called anonymousDelegate that returns an instance of an anonymous object implementing the ReadWriteProperty interface for a property of type String. The anonymous object has a property called curValue that is used to store the current value of the delegated property. The getValue function returns the value of this property, while the setValue function updates its value and prints a message to the console indicating the new value of the property.
You might have noticed that we are delegating properties directly from inside the main function and they are not declared inside a class. This is another feature provided by Kotlin, and it is called local delegated properties, which means you can use delegated properties not only in classes, but also as local variables within functions.
Delegating to another property
In Kotlin, you can delegate a property to another property, allowing you to reuse the behavior of an existing property for a new property. To delegate a property to another property, we use the by keyword followed by the name of the property you want to delegate to. Here's an example that demonstrates how to delegate a property to another property:
class Example {
private var _counter = 0
var counter: Int
get() = _counter
set(value) {
_counter = value
println("Counter set to $value")
}
var anotherCounter: Int by this::counter
}
fun main() {
val example = Example()
example.anotherCounter = 5 // Counter set to 5
}
In this example, we define a class called Example, which has two properties: counter and anotherCounter. The counter property is backed by a private field called _counter and has a custom getter and setter. The getter simply returns the value of the _counter field, while the setter updates the value of the _counter field and prints a message to the console indicating the new value of the property.
The anotherCounter property is delegated to the counter property using the by keyword followed by this::counter, which refers to the counter property of the current instance of the Example class. This means that when we get or set the value of the anotherCounter property, it will actually get or set the value of the counter property.
We can delegate to a property of another class by writing the name of that class instead of this.
Best practices
Here are some best practices to follow when using property delegation in Kotlin:
1) Use standard delegates when possible. The Kotlin standard library provides several useful kinds of delegates, such as lazy properties, observable properties, and storing properties in a map. These standard delegates can help you implement common functionality more easily and efficiently. These standard delegates are covered in another topic.
2) Keep your delegated properties focused. Each delegated property should do one thing well. Avoid creating "god-like" delegates that try to do too much, as they can become hard to maintain and test.
3) Avoid unnecessary delegation. While property delegation is a powerful feature, not every property needs to be delegated. Use delegation when it provides a clear benefit, such as reducing boilerplate code or improving performance. Overusing property delegation can make your code harder to read and understand.
Conclusion
In this topic, we've learned about the power and flexibility of property delegation in Kotlin. We've seen how delegation can be used to extend beyond classes to properties, allowing for delegated functionality to be abstracted out and shared between multiple similar properties.
We've explored how to implement custom property delegates by defining the getValue() and setValue() methods. We've also learned about anonymous delegates and how to use the ReadOnlyProperty and ReadWriteProperty interfaces from the Kotlin standard library to create delegates as anonymous objects.
Finally, we've seen how to delegate a property to another property, allowing us to reuse the behavior of an existing property for a new property. All that demonstrates the power and flexibility of property delegation in Kotlin and shows how it can be used to write more efficient and maintainable code.