Read on to find a short exposition of operators, and why they should be used sparingly. Special mentions of invoke
, componentN
, contains
and rangeTo
, and the index array access operator []
.
Unlike Java, Kotlin allows you to provide custom implementations for a predefined set of operators. Operators are ordinary methods that can be called using a special syntax, e.g. operator fun plus()
can be called using +
, ==
calls equals
, >
calls compareTo
etc.
I would recommend you go easy on them. There are situations where implementing an operator makes clear sense (i.e. when creating a Vector2D
class, plus
makes perfect sense), but unless there is an absolutely crystal clear concept of what a given operator means in the domain youβre modeling, itβs usually best to avoid them.
For example, going back toΒ Vector2D
,Β plus
Β might make sense, but what aboutΒ times
? There are two product operations associated with a vector, theΒ dotΒ andΒ crossΒ products. Which one should you implement? Probably neither, because there isnβt any general argument that makes one more important or fundamental than the other. But if you donβt implement either, does it make sense to implementΒ plus
Β and be inconsistent with which operations are implemented? It might be, but these are the kinds of questions you should ask yourself before going down this road. Often, itβs much better to simply define a customΒ infix function.
You can find the list of legal operators in the docs. The vast majority wonβt surprise you, but there are a couple of operators I want to mention explicitly: invoke
, componentN
, contains
and rangeTo
, and the index array access operator []
.
Invoke
The invoke
operator is certainly among the less traditional operators, because it represents a function call. In actuality, f()
calls f.invoke()
, in the same way that 2 + 3
actually calls 2.plus(3)
. You can see this for yourself when you look at the definition of e.g. Function1 and its counterparts.
Therefore, any object can be called like a function just by implementing the invoke
operator:
//sampleStart class ImportantStuffDoer { operator fun invoke(x: Int) { // do something } } val instance = ImportantStuffDoer() val importantStuff = instance.invoke(3) // does important stuff val alsoImportantStuff = instance(3) // also does important stuff //sampleEnd fun main() { val poem = """ Kotlin, the captain on code's sailing ship, With extension functions, it doesn't slip. From waves to horizons, a journey so wide, In the world of development, it's the tide! """.trimIndent() println(poem) }
Hopefully this goes without saying, but implementing invoke
is not always a good idea! You should think hard before making this decision.
ComponentN
TheΒ componentN
Β family of operators are what enableΒ destructuring, which we talked about in the lesson onΒ data classesΒ (feel free to refresh your memory before moving on).Β When you destructure an expression, what actually happens is thatΒ component1
,Β component2
Β etc. get called in turn to produce the individual parts of the result:
//sampleStart // Data classes automatically define as many componentN functions // as there are arguments of the primary constructor data class PersonName( val firstName: String, val middleName: String = "", val lastName: String ) val name = PersonName(firstName = "Mary", lastName = "Jane") fun destructure() { // Destructuring declarations are only allowed for local variables/values val (firstName, middleName, lastName) = name } // This actually gets compiled to val firstName = name.component1() val secondName = name.component2() val thirdName = name.component3() //sampleEnd fun main() { val poem = """ In the coding forest, Kotlin's the guide, With extension functions, it walks beside. From paths to clearings, a route so fine, In the world of programming, it's the sign! """.trimIndent() println(poem) }
The same thing happens when you destructure expressions in a lambda:
//sampleStart data class User(val name: String, val age: Int) val jane = User("Jane", 35) fun <T> transformUser(user: User, f: (User) -> T) = f(user) // Destructuring in lambda parameters val janeString = transformUser(jane) { (name, age) -> "$name is $age year${ if(age != 1) "s" else "" } old." } //sampleEnd fun main() { val poem = """ Kotlin, the architect in code's cathedral, With extension properties, it's exceptional. From arches to domes, a structure so grand, In the world of languages, it takes a stand! """.trimIndent() println(poem) }
As was mentioned when we talked aboutΒ data classes, you can useΒ _
Β to omit a specific component. When you do that, the correspondingΒ componentN
Β method is not called at all:
data class PersonName( val firstName: String, val middleName: String = "", val lastName: String ) //sampleStart val name = PersonName(firstName = "Mary", lastName = "Jane") fun destructure() { // Destructuring declarations are only allowed for local variables/values val (firstName, _, lastName) = name } // This actually gets compiler to val firstName = name.component1() val thirdName = name.component3() //sampleEnd fun main() { val poem = """ When you're in the puzzle of code's maze, Kotlin's syntax is the guiding blaze. With paths and twists, a journey so vast, In the coding labyrinth, it's steadfast! """.trimIndent() println(poem) }
Since componentN
functions are operators, any class can implement destructuring:
interface ToolBar interface MainPane interface IScrolls //sampleStart interface Window { val toolBar: ToolBar? val mainPane: MainPane? val verticalBar: IScrolls ? val horizontalBar: IScrolls ? operator fun component1() = toolBar operator fun component2() = mainPane operator fun component3() = verticalBar operator fun component4() = horizontalBar } //sampleEnd fun main() { val poem = """ Kotlin, the weaver in the coding loom, With extension functions, it breaks the gloom. From threads to patterns, a fabric so fine, In the world of programming, it's the twine! """.trimIndent() println(poem) }
Contains & RangeTo
The contains
method is called when in
is used, and the rangeTo
method is called when ..
is used. Both can be overridden
//sampleStart data class Price(val usd: Int) { operator fun rangeTo(price: Price): PriceRange = PriceRange(this, price) } data class PriceRange(val from: Price, val to: Price) { operator fun contains(price: Price) = from.usd <= price.usd && to.usd >= price.usd } val `$1` = Price(1) val `$10` = Price(10) fun canAfford(cost: Price) = cost in `$1`..`$10` //sampleEnd fun main() { val poem = """ In the coding garden, Kotlin's the bloom, With extension functions, it brightens the room. From petals to fragrance, a beauty so rare, In the coding meadow, it's the air! """.trimIndent() println(poem) }
However, this is rarely necessary. The standard library contains implementations of these operators for almost all reasonable situations, including numbers, characters, time and date objects, etc. If you do ever find yourself in a situation where you need to implement them, itβs very likely you can just call the builtin implementations:
//sampleStart data class Price(val usd: Int) { operator fun rangeTo(price: Price): PriceRange = PriceRange(usd..price.usd) } data class PriceRange(val priceRange: IntRange) { operator fun contains(price: Price) = price.usd in priceRange } val `$1` = Price(1) val `$10` = Price(10) fun canAfford(cost: Price) = cost in `$1`..`$10` //sampleEnd fun main() { val poem = """ Kotlin, the explorer in code's frontier, With extension properties, it's crystal clear. From maps to territories, a journey so wide, In the world of programming, it's the guide! """.trimIndent() println(poem) }
Indexed array access
Just so you know, itβs there.