Like many other modern languages, Kotlin supports operator overloading. However, unlike some other languages, it doesn't let you use arbitrary symbols as operators. Instead, it confines you to a predefined set of operators that you may define for each type.
Building on its operator semantics, the language also has a set of conventions that let you implement your own classes in ways that make them as convenient to use as first-party constructs, such as the List or Array types.
Our first example to learn operators with will be a class representing a Complex number, which consists of a real part and an imaginary part, two Double values.
For a start, we'll define a constructor that takes these two values, and a toString method which can print them nicely:
class Complex(val re: Double, val im: Double) {
override fun toString() = "$re${if (im < 0) "" else "+"}${im}i"
}What can we do if we want to add two Complex instances together? We can define a method for it. This method will create a new Complex instance, containing the result.
class Complex(val re: Double, val im: Double) {
// ...
fun add(other: Complex): Complex {
return Complex(this.re + other.re, this.im + other.im)
}
}We can then create instances of Complex, add them up, and print this result (which invokes toString):
val a = Complex(4.0, 3.0)
val b = Complex(8.0, 2.0)
println(a.add(b)) // 12.0+5.0iThis, of course, works as expected. But what we really want to do is use the operators that built-in types are using with our very own types:
println(a + b)The key to this in Kotlin is the operator keyword. This lets you define every operator present in the language (+, -, %, and so on) for your own types. You can not create operators with arbitrary symbols, you can only work with the predefined set.
To define these operators, you need a function that's marked with the operator keyword, and it needs to have a specific name that corresponds to an operator. You can find the mapping between the operator symbols and their function names in the official documentation.
The mapping between the method names and the syntax of each operator has semantic importance. You should only use operators for cases where calling the functions with their regular syntax (i.e. by name) would be appropriate as well. For example, don't define a plus method that doesn't add things together. After all, these are just functions, and they can still be invoked with the regular function call syntax at any time.
For our purposes of a + operator, we'll need to define the plus function, marked as an operator:
operator fun plus(other: Complex) =
Complex(this.re + other.re, this.im + other.im)The plus method has to be either a member or an extension on a type, and it has to take a single parameter. These are fixed requirements, since it's a binary operator.
There are no specific requirements for the types used in the method signature though. While this function takes another Complex instance as its parameter, and returns Complex, most Kotlin operators let you vary these types arbitrarily. For example, we could create a plus method that lets us add an Int or Double value to a Complex number:
operator fun plus(other: Double) = Complex(this.re + other, this.im)This will let you write code like this:
println(Complex(3.0, 9.0) + 20.0) // 23.0+9.0iThese were binary operators, but the language also has unary operators, which are applied to just a single value. For example, the unary minus operator, which negates a number (or gives you its additive inverse, if you're feeling fancy).
As already mentioned, operators don't have to be defined as members, they can also be extensions on a type (as long as they don't need to access private members):
operator fun Complex.unaryMinus() = Complex(-re, -im)This operator can then be used for your custom type just like it's used on the built-in types, by prefixing them with it:
val x = Complex(10.0, 20.0)
println(-x) // -10.0-20.0iWe created an operator to add Complex and Double instances together earlier, but the opposite doesn't work yet, as Double doesn't have a plus method that could accept a Complex parameter. This again is a great case for an operator defined as an operator:
operator fun Double.plus(other: Complex) = Complex(this + other.re, other.im)Now we can add a Complex to a Double:
println(20.0 + Complex(3.0, 9.0)) // 23.0+9.0iAs operators can be defined as extensions, the Kotlin Standard Library ships operators for many built-in types, without modifying them. Some examples of these are the java.math.BigDecimal and java.math.BigInteger extensions, as well as the plus and minus operators that are defined for many, many collection types.
These let you transform code like this...
fun solve(
a: BigDecimal, b: BigDecimal, c: BigDecimal
): Pair<BigDecimal, BigDecimal> {
val sqrtD = b.multiply(b).subtract(BigDecimal(4).multiply(a).multiply(c)).sqrt(MathContext.UNLIMITED)
val x1 = b.negate().add(sqrtD).divide(BigDecimal(2).multiply(a))
val x2 = b.negate().subtract(sqrtD).divide(BigDecimal(2).multiply(a))
return Pair(x1, x2)
}... into code like this:
fun solve(
a: BigDecimal, b: BigDecimal, c: BigDecimal
): Pair<BigDecimal, BigDecimal> {
val sqrtD = sqrt(b * b - 4.toBigDecimal() * a * c)
val x1 = (-b + sqrtD) / 2.toBigDecimal() * a
val x2 = (-b - sqrtD) / 2.toBigDecimal() * a
return Pair(x1, x2)
}The Int.bd extension property we've seen in chapter 4 would make this code even cleaner.
For the record, this is not a proper implementation of a quadratic equation solver. Write a better one as practice!
You might have noticed that the "indexing" syntax that we use when accessing elements in an array also works for other collections in Kotlin, for example, for the List types:
val numbers = mutableListOf(3, 5, 1, 6, 7, 2)
println(numbers[3])
numbers[4] = 8These, again, are a pair of operators: the get and set operators.
You can define these for your own types as well, and as they're just functions, you can place any code inside them. For example, you can have a Vector3 class, which stores its data as x, y, and z properties internally, but exposes these components through Int indices through operators.
data class Vector3(var x: Int, var y: Int, var z: Int) {
operator fun get(index: Int): Int {
return when (index) {
0 -> x
1 -> y
2 -> z
else -> throw IllegalArgumentException("Invalid index")
}
}
operator fun set(index: Int, value: Int) {
when (index) {
0 -> x = value
1 -> y = value
2 -> z = value
else -> throw IllegalArgumentException("Invalid index")
}
}
}You could then use a Vector3 instance like this:
val v = Vector3(0, 0, 0)
v[2] = 42
v[0] = 169
println(v[2]) // 42
println(v) // Vector3(x=169, y=0, z=42)You can define an arbitrary number of parameters for these operators. For example, for a
Matrixtype, you could achieve syntax likemx[4, 1] = 20, with a 3-parametersetmethod. All but the last parameter of thesetoperator have to be provided between the brackets at the call site.
A special case of operators is the invoke operator, which lets you use the function call syntax on the type that it's defined for. We've already encountered this operator for the Function types in the Kotlin runtime, back in chapter 4:
interface Function0<R> {
operator fun invoke(): R
}This is why a Function0 instance - which is anything with a function type that takes no parameters, such as () -> Unit - can be called as a function.
We can also use invoke in our own types if it makes sense. For example, if you're using the command pattern, you might define an interface like this:
interface Command {
operator fun invoke()
}As an example, you could write a command that can copy a file, implemented as simply as using copyTo:
class CopyCommand(
private val source: File,
private val target: File
) : Command {
override fun invoke() {
source.copyTo(target)
}
}You can invoke a command like this by calling its invoke method as a function, or using its operator form:
val command: Command = CopyCommand(File("current.txt"), File("archived.txt"))
command.invoke()
command()Let's create a Time class, which we'll use to learn more about both operators and conventions. This class represents a time of day as hours and minutes, but stores just a single value internally. Whenever hours or minutes are accessed, those values will be computed from totalMinutes.
class Time(hours: Int, minutes: Int) {
private val totalMinutes = hours * 60 + minutes
val hours: Int
get() = totalMinutes / 60
val minutes: Int
get() = totalMinutes % 60
}This class currently fails basic equality checks:
val sixAm = Time(6, 0)
val sixOClock = Time(6, 0)
println(sixAm == sixOClock) // falseThis would be expected in Java, where == is used to compare references. However, in Kotlin, this is actually a call to the equals method, defined in Any. This is done because this is the comparison we want to perform in our applications a vast majority of the time.
How do you perform a comparison of references, if
==is just anequalscall? With the===and!==operators.
In this specific case, as we don't have an equals method defined for Time yet, this call will fall back to its default implementation in Any, which simply compares references by default. But now that we know which method is being called, we can fix the comparison!
One approach to get an equals implementation quickly would be to make Time a data class, but this doesn't work with the current design where its data isn't stored in constructor properties. We'll implement equals separately instead:
override fun equals(other: Any?): Boolean {
if (other !is Time) return false
return totalMinutes == other.totalMinutes
}
override fun hashCode(): Int {
return totalMinutes
}We are following age-old advice from Effective Java here: Item 11: Always override
hashCodewhen you overrideequals.
Our equals implementation isn't quite perfect (compare it with what IntelliJ generates, if you want to see some potential improvements), but it's good enough for most practical purposes. Note how we're taking advantage of smart casts in it, to access other.totalMinutes.
Implementing equals gives us working == and != operators for our Time class:
val sixAm = Time(6, 0)
val sixOClock = Time(6, 0)
println(sixAm == sixOClock) // true
println(sixAm != sixOClock) // falseHow about other comparisons? These are provided through yet another operator, compareTo. This operator has to take a single parameter, and must return an Int, the value of which follows the rules set by the java.lang.Comparable interface. Simplified, this return value should be:
- zero, if the two values are equal,
- a negative number, if the first value is smaller,
- a positive number, if the first value is larger.
Implementing this for Time is easy, as we can just subtract their two stored values from each other:
operator fun compareTo(other: Time): Int {
return totalMinutes - other.totalMinutes
}If we get a bit cheeky, we can delegate this to the
Int#compareTofunction:operator fun compareTo(other: Time): Int { return totalMinutes.compareTo(other.totalMinutes) }
With this in place, we now get to use >, <, <= and >= on Time:
println(Time(5, 0) < Time(6,0)) // true
println(Time(5, 0) >= Time(10, 25)) // falseOptionally, you can also implement the Comparable interface, which comes in handy in certain cases, for example, if you want to sort a collection of such items. The compareTo method declared in the interface matches our existing operator already, so if you want to make Time a Comparable, all you need to add is the override keyword:
class Time(hours: Int, minutes: Int): Comparable<Time> {
// ...
override operator fun compareTo(other: Time): Int {
return totalMinutes - other.totalMinutes
}
}We've just looked at what == and === do, so this is a good place to take a closer look at the regular old = operator. In Java, assignments are expressions (something with a return type, which yields a return value when evaluated), and they return the newly assigned value. This is why you can write code like this:
BufferedReader reader = ...;
String line = null;
while ((line = reader.readLine()) != null) { // assignment used as an expression
System.out.println(line);
}You might have noticed that we didn't do this in chapter 7 when we were reading lines from a file. In Kotlin, assignments are only statements (a valid line of code that can be executed), but not expressions. This language design decision follows the frequent advice of not using the return value of an assignment as an expression even if a language allows it, as this can be hard to read, and it's a potential source of bugs.
For example, this code in C is just a single character away from checking whether x is 0 and running a branch accordingly. Instead, it always sets x to 0 and then runs the else branch, as the value of the x = 0 expression is automatically coerced into false:
if (x = 0) {
// Run if X was zero...
} else {
// Run if X is non-zero...
}Augmented assignments like += are translated to operator functions as well, such as to plusAssign.
If these types of augmented function operators don't exist, the assignments get unrolled instead. For example, a += b is unrolled to a = a + b, and a suitable binary plus operator function is used between the values, before performing a simple assignment.
These operators are used extensively with collections in Kotlin. For mutable collections, using them mutates the contents of the collection in-place:
val mutable = mutableListOf(2, 3, 5, 7)
mutable += 11For read-only collections, however, using an augmented assignment results in the creation of new collections with updated contents. The variable is then reassigned to the new collection - this is only possible if it's declared as a var.
var readOnly = listOf(2, 3, 5, 7)
readOnly += 11This highlights an important decision when working with lists that you want to change over time: whether to use a single MutableList stored in a val and mutate its contents over time, or use a mutable var and change the list instance it points to when necessary. Both can be valid choices depending on the exact use case.
What's almost certainly a bad idea is the combination of the two: creating a var which contains a mutable collection. It gets very difficult to reason about how such a list might change.
// Avoid doing this! Both the entire list instance and its contents may change
var myEverChangingList = mutableListOf(1, 2, 3)We've seen how basic operators work, let's move on to some more advanced ones, which we'll discuss under the theme of conventions. These conventions will define further, special operators, which will let you empower your own types, and make them play just as smoothly with built-in language features as first-party types do.
The first convention we want to use is the range convention, to define a given interval of time. We already know that the .. syntax can create a range from two Int values. This range can be iterated over, and we can also check if an Int is within the range:
val range = 0..10
for (i in range) { ... }
if (5 in range) { ... }Let's do something similar for our Time class, step by step. If we look at the type of range, we'll see that it's an instance of an IntRange. Similarly, we'll create a TimeRange class that will represent our interval, between two points in time:
class TimeRange(private val start: Time, private val end: Time)The .. operator used to create a range maps to the rangeTo operator function, which we can define either as a member or an extension. We can go with an extension for this one - this way, the Time class can stay simpler, and we can add the range functionality to it without modifying it:
operator fun Time.rangeTo(other: Time): TimeRange {
return TimeRange(this, other)
}With this defined, we can already create a TimeRange:
val morning = Time(8, 0)
val evening = Time(17, 0)
val range: TimeRange = morning..eveningLet's see how we can check if the range contains a given value. Whenever the in keyword is used, it will actually map to a call of an operator called contains, by convention. This method has to exist on whatever is on the right-hand side of in, and it will receive the value on its left as its parameter. So x in y is equivalent to a y.contains(x) call.
We can implement this operator in our TimeRange:
class TimeRange(private val start: Time, private val end: Time) {
operator fun contains(time: Time): Boolean {
return start <= time && time <= end
}
}Putting this new method to the test, we can see that we also get !in for free:
val lunch = Time(12, 30)
println(lunch in range) // true
println(lunch !in range) // falseSince
TimeimplementsComparable, we actually get a range with this functionality for free, through theClosedRangetype of the Standard Library. Try running the code above without theTimeRangeimplementation in your project to see that in action!
Let's move on to iteration. When we use the for (x in y) syntax, we are again invoking functions under the hood. This syntax is equivalent to the following:
val iterator = y.iterator()
while (iterator.hasNext()) {
val x = iterator.next()
// for loop body for `x`
}First, we'll need to create an operator method in TimeRange called iterator, which - as you might guess - has to return an Iterator:
class TimeRange(private val start: Time, private val end: Time) {
// the `contains` implementation...
operator fun iterator(): Iterator<Time> {
TODO("implement")
}
}How do we provide an Iterator? We can implement the interface ourselves right here in the method, with an object expression. An Iterator has to define the two methods that you can see used in the pseudocode snippet above:
hasNext()returns aBoolean, and tells us whether there are any elements remaining,next()fetches the next element from whatever we're iterating.
Our iterator will iterate on time values minute by minute within our range. Here's our implementation:
operator fun iterator(): Iterator<Time> {
return object : Iterator<Time> {
// 1
val startMinutes = start.hours * 60 + start.minutes
val endMinutes = end.hours * 60 + end.minutes
// 2
var currentMinutes = startMinutes
// 3
override fun hasNext(): Boolean {
return currentMinutes <= endMinutes
}
// 4
override fun next(): Time {
return Time(hours = currentMinutes / 60, minutes = currentMinutes % 60).also {
currentMinutes++
}
}
}
}Here, we...
- Calculate the start and end times of our range in total minutes.
- Keep track of where our iteration is in a mutable property.
- Say that there are more time instances to iterate through as long as the current value didn't pass the max value.
- Create a new
Timeinstance every time the iterator is asked for the next item.
We didn't add a check for whether we still have elements remaining in the
nextmethod. This isn't an issue when our range is used in a loop, but it could cause issues if the iterator is used manually, through function calls. Our KotlinIteratormaps tojava.util.Iterator, which specifies thatnextshould throw aNoSuchElementExceptionif there are no more elements to return.
Note that we are at no point storing all the Time instances between the min and max values of the range (neither does IntRange actually contain all Int values between its bounds). We are just lazily creating these intermediate values, as the for loop is progressing on our range.
This code could be simpler if we had an
incrementoperator in ourTimeclass. Write that operator, and then refactor this code!
Let's put our iteration code to work:
for (time in morning..lunch) {
println(String.format("%02d:%02d", time.hours, time.minutes))
}Kotlin doesn't offer its own string formatting syntax at this time. However, as long as we're on the JVM, we can make use of the same format strings that Java offers.
Just like with compareTo and Comparable, you can optionally implement an interface called Iterable here, which declares the iterator method with a matching signature to the operator. Implementing this interface is very powerful. It will grant you access to many of the extensions of the Kotlin Standard Library's collection API, as most of these are defined on Iterable.
For example, you could take a TimeRange, and quickly get a List<Time> that only contains the instances of Time from it that are at a round 15 minutes:
(morning..lunch).filter { it.minutes % 15 == 0 }So the general takeaway from these optional interfaces:
- If you define the right operators, then by convention, you get to use your own types with certain language features.
- If you implement these interfaces, you'll also get access to a vast number of extensions that are defined on those interfaces.
The operators that enable conventions can be added as extensions, so you can make existing types that you don't own comparable with
>or iterable within. However, you can't make types implement interfaces after-the-fact.
The built-in range types are also Iterable. This means that you can, for example, use forEach on them to run some code for a certain range of indices:
(10..20).forEach {
println(it)
}A method reference could clean this up even further:
(10..20).forEach(::println)Similarly, the map extension can also come in handy, to create a number of instances of some type, based on the indices:
val times = (5..15).map { min -> Time(8, min) }One last convention we can apply here is destructuring. We've already seen that data classes and lists can be destructured. But this feature, again, seems like something deeply baked into the language, with strong integration in basic types. Thankfully, destructuring also works through operator conventions!
For each index that you want to be able to destructure, you have to define a method with a corresponding name: component1, component2, and so on. For our Time class, we can define these methods even as extensions, to be able to destructure it into an hour and a minute value:
operator fun Time.component1(): Int = hours
operator fun Time.component2(): Int = minutesNote that these component functions are not 0-indexed... For some reason.
This lets us destructure a Time instance like this:
val (hours, minutes) = Time(13, 42)Remember that destructuring is positional, and not based on the names of the variables you're declaring in any way.
Destructuring can also be used in lambda parameters and in for loops, so we can now update our previous loop like this:
for ((hours, minutes) in morning..lunch) {
println(String.format("%02d:%02d", hours, minutes))
}Note the power that lies in component accessors being methods. They aren't restricted to returning values stored in the class, as-is. They can compute values on-the-fly, return values with different types, and so on. Anything that a function can do can be done when a component is being accessed during destructuring.
Destructuring support for collection types is also added as extensions.
Based on what we've just seen, we know that code like this:
for (i in 0..10) {
println(i)
}... will create an IntRange instance, and then grab an Iterator from it, which it uses to run the for loop. This is multiple allocations and several method calls, while the Java equivalent of this loop has none:
for (int i = 0; i <= 10; i++) {
System.out.println(i);
}If we decompile the bytecode generated from the Kotlin loop above, we can be relieved:
int i = 0;
for(byte var11 = 10; i <= var11; ++i) {
System.out.println(i);
}Simple usages of the built-in ranges are recognized and optimized by the compiler, so that no unnecessary allocations occur. If you assign an IntRange to a variable, and pass it around as a parameter, that will result in an actual object allocation, as something has to be passed around.
We've just uncovered the inner workings of operators and ranges, and seen that we can do everything the built-in types could do for our own types as well. It's time to do the same thing for property delegates!
We've looked at the built-in ones provided by the Standard Library earlier, and we'll now see that there's actually nothing special about them: it's conventions all the way down.
Let's start by reimplementing one of them, the lazy delegate. Recall the example we used earlier, of computing the value of pi lazily? The lazy delegate let us only perform the computation when we first access the value of pi, and then it stored its value for us, so that any subsequent calls to the property were quick.
val pi by lazy {
print("Computing... ")
sqrt(6 * (1..1_000_000_000).sumOf {
1.toDouble() / it / it
})
}
println(pi) // Computing... 3.14159264498239
println(pi) // 3.14159264498239
println(pi) // 3.14159264498239So how do delegates work? They extract the logic of a property's getter (and setter, if the property isn't a read-only val) into a class. For a class to be eligible for use as a delegate that backs a property, it needs to define a specific operator function, which will be called whenever the value of the property it backs is being read. This, again defined by convention, is the following getValue function:
class Lazy<T> {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
TODO("implement getter")
}
}We've created a class called Lazy with a generic parameter (T), which will be the type of the value that it computes lazily. The getValue function has to have two very specific parameters, we'll explore these later. What we need to care about for now is its return type, which is up to us to define - we'll choose the T type here, so that this class can back a property of that type.
We've worked with the backing field using the field identifier in custom getters and setters already, but we don't get this kind of field when using delegates, as how we implement the getter and setter is completely up to us.
We might never store a value in a delegate - just like in a custom getter or setter.
If we do want to store a value, we'll simply declare it as a property in our delegate class. We'll also add a constructor parameter to our lazy delegate: the function that can compute the value of the property when needed.
class Lazy<T>(private val initializer: () -> T) {
private var value: T? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (value == null) {
value = initializer()
}
return value!!
}
}Inside the getValue function, we check if our value property is already storing a value, and if it isn't, we initialize it using the initializer function provided. Then, we return the stored value.
Here's what the usage of this delegate would look like:
val pi by Lazy({
println("Computing")
sqrt(6 * (1..1_000_000_000).sumOf {
1.toDouble() / it / it
})
})Of course, we can drop the parentheses around the lambda that we're passing in to the constructor, to get closer to the original syntax. We also want to have a lowercase lazy there, which we could achieve by renaming the class... Or we can do what the Standard Library does, and introduce a factory function for our delegate instead:
fun <T> lazy(initializer: () -> T) = Lazy(initializer)This gets us back to the original syntax at the use site:
val pi by lazy {
println("Computing")
sqrt(6 * (1..1_000_000_000).sumOf {
1.toDouble() / it / it
})
}Implementing lazy was a bit difficult, as we had to get the signature of getValue just right, off the top of our head (or at least by going to the documentation, and copying it from there). Thankfully, there's a better way: the Standard Library provides a pair of interfaces for delegates, the first one being ReadOnlyProperty, which you can use when implementing delegates that will only ever be used as a val.
public interface ReadOnlyProperty<in T, out V> {
public operator fun getValue(thisRef: T, property: KProperty<*>): V
}The second generic parameter, V corresponds to the type of the property that's being delegated, and therefore, the return type of the getValue is defined as V.
T is a more interesting type parameter in the delegate interfaces, which is being used as the type of the thisRef parameter. The name thisRef is quite descriptive. Whenever getValue is called, this first parameter will be a this-reference, i.e. the reference of the instance that contains this property. This value will be null if the property doesn't belong to a class instance, which can happen if it's top-level or local. If you use a parameter that's more concrete than Any, you can restrict the types of classes that the delegate can be used in.
Note that if you don't make this parameter nullable, your delegate will not be available for use in top-level or local declarations.
Receiving this containing instance as a parameter means that you can access its state or methods while you're performing your getter's tasks, which can be a powerful tool to have.
For one example, on Android, you might restrict this
Tparameter toContext, the class which allows access to system resources, so that your delegate can make use of that in its implementation.
We can now update our Lazy delegate to use this interface:
class Lazy<T>(private val initializer: () -> T) : ReadOnlyProperty<Any?, T> {
private var value: T? = null
override operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (value == null) {
value = initializer()
}
return value!!
}
}Thanks to this interface, we can also hide this concrete class from clients, by using the interface as the return type of the lazy factory function:
fun <T> lazy(initializer: () -> T): ReadOnlyProperty<Any?, T> = Lazy(initializer)With the implementation of
lazyas a starting point, try reimplementingDelegates.observableandDelegates.vetoableas practice!
The Standard Library
lazyimplementation is more sophisticated than our custom one. For example, it lets you store nullable values lazily (which ours would fail at, always recomputing the value), and it's also thread-safe by default. Its implementation is worth taking a look at.
We've covered read-only delegates above. Delegates can also be used as read-write properties, and there's a corresponding ReadWriteProperty interface for this use case.
This interface extends ReadOnlyProperty, and adds a setValue method:
public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
public override operator fun getValue(thisRef: T, property: KProperty<*>): V
public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}Learn more about read-write delegates and an advanced way of creating delegate instances in the extras of this chapter.
Finally, to wrap up the topic of delegates, let's talk about a small feature of the Standard Library: delegation to Map instances. The Map type knows nothing about delegates (nor should it!), but there are setValue and getValue operators defined for it as extensions.
The fact that using the delegate interfaces are optional for the delegation conventions to work comes in really handy here. You wouldn't be able to make
Mapimplement an interface, but you can add operators to it as extension functions.
How does delegation to a Map work? For example, we can set up a class that holds all of its data in a Map that it receives as a parameter:
class Address(map: MutableMap<String, String>) {
var country: String by map
var zip: String by map
var city: String by map
var street: String by map
}Each of these properties will be stored in the map, with the name of the property being used as the key (using the property parameter of the accessors that we've seen above).
val map = mutableMapOf<String, String>()
val address = Address(map)
address.country = "Hungary"
address.city = "Budapest"
address.zip = "1117"
println(address.city) // BudapestStoring and reading values works as expected. And if you were to print the entire contents of the map instance, you'd see this:
{country=Hungary, city=Budapest, zip=1117}
Kotlin offers many convenient features for its first-party types: various kinds of operators, integration with language features such as iteration with the for loop or use in destructuring, and more. The delegates of the Standard Library are also similarly tightly integrated, seemingly magical parts of the language.
All of these things, however, are available for anyone to use, by implementing the right operators and aligning with certain conventions. This enables your own types to be just as idiomatic and convenient to use as any built-in type. You can also improve the API of existing types, by adding these conventions to them as extensions.
- Delightful Delegate Design
- Official documentation