Kotlin, being a modern and expressive programming language, provides a set of conventions that allow developers to use specific language constructs by defining functions with predefined names. These conventions provide a consistent and intuitive way to work with various language features. In this article, we’ll explore the different aspects of conventions in Kotlin and provide in-depth explanations along with examples.
Kotlin Conventions
As you know, Java has several language features tied to specific classes in the standard library. For example, objects that implement java.lang.Iterable can be used in for loops, and objects that implement java.lang.AutoCloseable can be used in try-with-resources statements. Kotlin has a number of features that work in a similar way, where specific language constructs are implemented by calling functions that you define in your own code. But instead of being tied to specific types, in Kotlin those features are tied to functions with specific names. For example, if your class defines a special method named plus, then, by convention, you can use the + operator on instances of this class. Because of that, in Kotlin we refer to this technique as conventions.
Kotlin uses the principle of conventions, instead of relying on types as Java does, because this allows developers to adapt existing Java classes to the requirements of Kotlin language features. The set of interfaces implemented by a class is fixed, and Kotlin can’t modify an existing class so that it would implement additional interfaces. On the other hand, defining new methods for a class is possible through the mechanism of extension functions. You can define any convention methods as extensions and thereby adapt any existing Java class without modifying its code
BTW, conventions in Kotlin are not limited to operator overloading or extension functions. There are various predefined function names that, when implemented in your class, enable specific language constructs and provide additional functionality. Here are a few examples:
iterator
: By implementing theiterator
function, you enable the usage of your class infor
loops, similar to how objects implementingjava.lang.Iterable
work in Java.invoke
: Defining theinvoke
function allows you to treat instances of your class as function-like objects. This means you can call objects of your class as if they were functions directly, likemyObject(parameter)
.compareTo
: If you implement thecompareTo
function, you can use comparison operators (<
,<=
,>
,>=
) on instances of your class, allowing for natural ordering.- Destructuring Declarations: Kotlin provides the ability to destructure objects into multiple variables using the
component1()
,component2()
, etc. functions. By implementing these functions in your class, you can use destructuring declarations with instances of your class.
These are just a few examples of Kotlin conventions. By defining functions with specific names in your class, you can harness the power of Kotlin’s language features and make your code more expressive and concise. Kotlin conventions simplify the process of working with common language constructs and operators, promoting code readability and reducing boilerplate. Let’s go through each topic step by step.
Overloading arithmetic operators
As we see earlier, the most straightforward example of the use of conventions in Kotlin is arithmetic operators. In Java, the full set of arithmetic operations can be used only with primitive types, and additionally, the + operator can be used with String values. But these operations could be convenient in other cases as well. For example, if you’re working with numbers through the BigInteger class, it’s more elegant to sum them using + than to call the add method explicitly. To add an element to a collection, you may want to use the += operator. Kotlin allows you to do that, and in the below section, we’ll see how it works.
Overloading binary arithmetic operations
In Kotlin, you can overload binary arithmetic operations by defining functions with the corresponding operator modifier. That means Arithmetic operators, such as +
, -
, *
, /
, and %
, can be overloaded in Kotlin. To overload an arithmetic operator, you need to define a function with a specific name and annotate it with the operator
keyword. Let’s take the example of overloading the plus
operator for the Point
class.
data class Point(val x: Int, val y: Int)
You can define the plus
function within the Point
class as follows:
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
Note that the
operator
keyword is used to declare theplus
function. This explicitly indicates that you intend to use the function as an implementation of the+
operator convention.
Once you have declared the plus
function as an operator, you can use the +
sign to sum up instances of the Point
class:
val point1 = Point(1, 2)
val point2 = Point(3, 4)
val sum = point1 + point2
println(sum) // Output: Point(x=4, y=6)
The point1 + point2
expression is equivalent to point1.plus(point2)
. The plus
function is called automatically when the +
operator is used between two Point
instances.
Under the hood, when you use the +
operator, the plus
function is automatically invoked to perform the addition.
Alternatively, you can define the plus
operator function as an extension function outside the Point
class:
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
The implementation of the operator function remains the same, but it is now defined as an extension function.
Lists of all the binary operators you can define and the corresponding function names:
You can overload other arithmetic operators in a similar way by defining corresponding functions (minus
, times
, div
, rem
, etc.) within your class.
It’s important to note that the precedence of Kotlin operators follows the same rules as the standard numeric types. For example, in an expression like a + b * c
, the multiplication (*
) will always be executed before the addition (+
), regardless of how you have defined those operators, even if you’ve defined those operators yourself. The operators *, /, and % have the same precedence, which is higher than the precedence of the + and — operators
Kotlin operators do not automatically support commutativity, which means you need to define separate operators if you want to swap the order of the operands. For example
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
val p = Point(10, 20)
println(p * 1.5) // Point(x=15, y=30)
Now suppose If you want users to be able to write 1.5 * p in addition to p * 1.5, you need to define a separate operator for that:
operator fun Double.times(p: Point): Point
Furthermore, The return type of an operator function can also be different from either of the operand types. For example, you can define an operator to create a string by repeating a character a number of times.
operator fun Char.times(count: Int): String {<br>return toString().repeat(count)<br>}<br><br>println('a' * 3) // aaa
This operator takes a Char as the left operand and an Int as the right operand and has String as the result type. Such combinations of operand and result types are perfectly acceptable.
Note that you can overload operator functions like regular functions: you can define multiple methods with different parameter types for the same method name.
Java Interoperability (Operator Functions and Java)
When it comes to calling Kotlin operator functions from Java, it is straightforward. Since each overloaded operator in Kotlin is defined as a function, you can call them from Java just like regular functions by using the full function name.
For example, let’s say you have a Kotlin class Point
with an overloaded plus
operator function:
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
In Java, you would call this operator function as a regular function using the fully qualified name:
Point point1 = new Point(1, 2);
Point point2 = new Point(3, 4);
Point sum = point1.plus(point2);
Since Java doesn’t have a specific syntax for operator functions like Kotlin does, you need to call the operator function using the actual function name (
plus
in this case) instead of the operator symbol (+
).
Now when calling Java from Kotlin, you can use the operator syntax for methods that match Kotlin conventions. Kotlin provides syntactic sugar to make code more concise and expressive. For example, if you have a Java class with a method named add
and you call it from Kotlin, you can use the +
operator instead, as long as the method follows the conventions of the plus
operator in Kotlin.
On the other hand, if a Java class defines a method with the desired behavior but with a different name, you can define an extension function in Kotlin with the correct name that delegates to the existing Java method. This allows you to bridge the gap between Kotlin’s operator conventions and Java’s method names.
So, when interacting between Kotlin and Java, Kotlin operator functions can be called from Java as regular functions using their fully qualified names. In the reverse scenario, when calling Java from Kotlin, you can use the operator syntax for methods that adhere to Kotlin’s conventions, and you have the flexibility to define extension functions in Kotlin to map to existing Java methods with different names.
No special operators for bitwise operations
In Kotlin, there are no special operators for bitwise operations on standard number types. Instead, regular functions with infix call syntax are used to perform bitwise operations. You can also define similar functions that work with your own types.
Kotlin provides a set of functions for performing bitwise operations, including:
shl
(Signed shift left): Performs a signed shift left operation.shr
(Signed shift right): Performs a signed shift right operation.ushr
(Unsigned shift right): Performs an unsigned (logical) shift right operation.and
(Bitwise and): Performs a bitwise and operation.or
(Bitwise or): Performs a bitwise or operation.xor
(Bitwise xor): Performs a bitwise exclusive or operation.inv
(Bitwise inversion): Performs a bitwise inversion operation.
Here’s an example demonstrating the use of some of these functions:
println(0x0F and 0xF0) // Bitwise and operation: 00001111 and 11110000 = 00000000 (result: 0)
println(0x0F or 0xF0) // Bitwise or operation: 00001111 or 11110000 = 11111111 (result: 255)
println(0x1 shl 4) // Signed shift left operation: 00000001 shifted left by 4 positions = 00010000 (result: 16)
In the above example, and
performs a bitwise and operation between 0x0F
(00001111) and 0xF0
(11110000), resulting in 0
(00000000). or
performs a bitwise or operation between the same values, resulting in 255
(11111111). shl
shifts 0x1
(00000001) left by 4 positions, resulting in 16
(00010000).
It’s important to note that while Kotlin doesn’t provide special operators for bitwise operations, you can use these functions to achieve the desired functionality. Additionally, you can define similar functions for your own types if needed.
Overloading compound assignment operators
In Kotlin, compound assignment operators such as +=
, -=
, and so on, are supported for certain operations. These operators allow you to modify a variable by performing an operation and assigning the result back to the variable in a single step.
When you define an operator, such as plus
, Kotlin automatically supports the corresponding compound assignment operator, such as +=
.
However, there can be ambiguity when both the regular operator function and the compound assignment operator function are applicable.
To illustrate, let’s consider the example of the plusAssign
function defined for a mutable collection in the Kotlin standard library:
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}
When you use the +=
operator with a mutable collection, the plusAssign
function is called. It adds an element to the collection.
In some cases, there may be ambiguity between using the regular operator or the compound assignment operator. When both the regular operator function (e.g., plus
) and the compound assignment operator function (e.g., plusAssign
) are applicable, Kotlin needs to decide which one to use.
To resolve this ambiguity, there are a few options:
- Replace the use of the operator with a regular function call that explicitly states the intention. For example, instead of using
+=
, you can use theadd
function directly:collection.add(element)
. - Change a
var
to aval
to make the compound assignment operation inapplicable. If the variable is read-only (val
), the compound assignment operator cannot modify its value. - Design your classes consistently by providing either the regular operator function or the compound assignment operator function, but not both. If your class is immutable, like the
Point
class in an earlier example, it\’s best to provide operations that return a new value (e.g.,plus
). On the other hand, if your class is mutable, like a builder, it\’s best to provide operations likeplusAssign
that modify the instance in place.
It’s important to note that the Kotlin standard library supports both approaches for collections. The +
and -
operators always return a new collection. The +=
and -=
operators work on mutable collections by modifying them in place and on read-only collections by returning a modified copy. However, it\’s worth mentioning that +=
and -=
can only be used with a read-only collection if the variable referencing it is declared as a var
and not as a val
.
val readOnlyList = listOf(1, 2, 3)
var mutableList = mutableListOf(4, 5, 6)
mutableList += readOnlyList
println(mutableList) // Output: [4, 5, 6, 1, 2, 3]
readOnlyList += mutableList // Error: Val cannot be reassigned
In the above example, we have a read-only list readOnlyList
and a mutable list mutableList
. The +=
operator can be used to add the elements of readOnlyList
to mutableList
because mutableList
is declared as a var
. However, when we try to use +=
on readOnlyList
, it throws an error because readOnlyList
is declared as a val
and cannot be reassigned.
So, to use
+=
and-=
operators on a read-only collection, make sure the variable referencing it is declared as a mutable variable (var
). If it is declared as an immutable variable (val
), you won\’t be able to modify the collection using these operators.
In Kotlin, when using the +=
and -=
operators on collections, you can provide either individual elements or other collections as operands. The important thing is that the elements or collections you use should have a matching element type with the collection you are modifying.
val numbers = mutableListOf(1, 2, 3)
numbers += 4
println(numbers) // Output: [1, 2, 3, 4]
val list1 = listOf(1, 2, 3)
val list2 = listOf(3, 4, 5)
val combinedList = list1 + list2
println(combinedList) // Output: [1, 2, 3, 3, 4, 5]
It’s important to note that when using collections as operands, the element types should match. In the above example, both list1
and list2
contain integers, so the operations work seamlessly. If the element types don\’t match, such as combining a list of integers with a list of strings, you will encounter a compilation error.
Overloading unary operators
Kotlin allows you to overload unary operators, which are applied to a single value, as in -a. You can overload unary operators such as unary minus (-
) and increment/decrement (++
and --
) by declaring a function with a predefined name and marking it with the operator
modifier.
Let’s start with the unary minus operator. To overload the unary minus operator for a class, you can define a function named unaryMinus
without any parameters. This function should negate the coordinates of the object and return it.
data class Point(val x: Int, val y: Int) {
operator fun unaryMinus(): Point {
return Point(-x, -y)
}
}
In the above example, the unaryMinus
function negates the coordinates of the Point
object and returns a new Point
with negated values.
Next, let’s consider the increment and decrement operators (++
and --
). When you define the inc
and dec
functions to overload these operators, the compiler automatically supports the same semantics as for regular number types, including both pre-increment and post-increment operations.
Here’s an example that demonstrates the use of pre-increment and post-increment operators:
data class Counter(var value: Int) {
operator fun inc(): Counter {
value++
return this
}
operator fun dec(): Counter {
value--
return this
}
}
fun main() {
var counter = Counter(0)
println(counter++) // Post-increment: prints the current value (0)
println(++counter) // Pre-increment: increments and then prints the new value (2)
}
In the above example, the inc
function overloads the ++
operator for the Counter
class. It increments the value
property and returns the updated Counter
object. Similarly, the dec
function overloads the --
operator for decrementing.
When the increment or decrement operator is used, the semantics depend on whether it is used before or after the operand. The post-increment operator (counter++
) evaluates the expression and then increments the value. The pre-increment operator (++counter
) increments the value and then evaluates the expression.
Overloading comparison operators
In Kotlin, you can overload comparison operators (such as ==
, !=
, >
, <
, >=
, <=
) for any object, not just for primitive types. This allows for intuitive and concise comparisons.
Equality operators: “equals”
Using the == operator in Kotlin is translated into a call of the equals method, also using the != operator is also translated into a call of equals, with the obvious difference that the result is inverted.
Equality operators (==
and !=
) can be used with nullable operands because they check for equality to null
internally. When using the comparison a == b
, it checks whether a
is not null and, if it\’s not, calls a.equals(b)
. If both arguments are null references, the result is true.
To check if two objects refer to the same instance, you can use the identity equals operator (
===
). It behaves similarly to the==
operator in Java and checks if both arguments reference the same object. However, note that the===
operator cannot be overloaded.
The equals
function is used to implement the equality comparison. It is marked as override
because it is defined in the Any
class, which is the base class for all objects in Kotlin. You don\’t need to mark it as an operator
because the base method in Any
is already marked as such, and the operator
modifier applies to all methods that implement or override it. It is important to note that equals
cannot be implemented as an extension function since the inherited implementation from Any
would always take precedence over the extension.
Ordering operators: compareTo
For ordering operators (<
, >
, <=
, >=
), Kotlin supports the Comparable
interface. The compareTo
method defined in this interface can be called by convention, and the usage of comparison operators is translated into calls to compareTo
. The return type of compareTo
must be Int
. For example, the expression p1 < p2
is equivalent to p1.compareTo(p2) < 0
. Other comparison operators work in a similar way.
Here’s an example of implementing the compareTo
function for a Person
class, using address book ordering:
data class Person(val firstName: String, val lastName: String) : Comparable<Person> {
override fun compareTo(other: Person): Int {
return if (lastName != other.lastName) {
lastName.compareTo(other.lastName)
} else {
firstName.compareTo(other.firstName)
}
}
}
fun main() {
val person1 = Person("Amol", "Pawar")
val person2 = Person("Govind", "Rakhonde")
println(person1 < person2) // true
println(person1 > person2) // false
}
In the above example, the compareTo
function compares Person
objects based on their last names first. If the last names are the same, it compares based on their first names. The Comparable
interface is implemented to enable comparisons using the concise operator syntax.
It’s important to note that you don’t need to add any extensions to compare Java classes that implement the Comparable
interface.
println("abc" < "bac") //true
Kotlin supports the operator syntax for these comparisons out of the box.
Conventions used for collections and ranges
Some of the most common operations for working with collections are getting and setting elements by index, as well as checking whether an element belongs to a collection. All of these operations are supported via operator syntax: To get or set an element by index, you use the syntax a[i] (called the index operator). The in operator can be used to check whether an element is in a collection or range and also to iterate over a collection. You can add those operations for your own classes that act as collections. Let’s now look at the conventions used to support those operations.
Accessing elements by index: “get” and “set”
In Kotlin, you can access and modify values in a map or a custom class using the indexing operator []
and the set
function. Let\’s look at how to implement these conventions:
Implementing the get operator:
- To access a value from a map using the indexing operator, you can define the
get
function in your class. - The
get
function takes the key as a parameter and returns the corresponding value. - You can define the
get
function with a single parameter of the key type and mark it asoperator
. - Inside the
get
function, you can handle different cases based on the key value and return the appropriate result.
operator fun Point.get(index: Int): Int {
return when (index) {
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
Implementing the set operator:
- To change the value for a key in a mutable map or a custom class, you can define the
set
function. - The
set
function takes the key and the new value as parameters and updates the corresponding value. - You can define the
set
function with two parameters (index and value) and mark it asoperator
. - Inside the
set
function, you can handle different cases based on the index and update the value accordingly.
operator fun MutablePoint.set(index: Int, value: Int) {
when (index) {
0 -> x = value
1 -> y = value
else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
With these implementations, you can use the indexing operator []
on objects of your class to access or modify specific values. For example:
val p = Point(5, 10)
val xCoordinate = p[0] // Accessing the x-coordinate of Point using the indexing operator
p[1] = 20 // Modifying the y-coordinate of Point using the indexing operator
val mutableP = MutablePoint(3, 7)
mutableP[0] = 15 // Modifying the x-coordinate of MutablePoint using the indexing operator
By adhering to the conventions and implementing the get
and set
functions, you enable the use of square brackets for element access and modification, providing a familiar and intuitive syntax for working with maps or custom collection-like classes.
The “in” convention
In Kotlin, the in
operator is used to check whether an object belongs to a collection or range. The corresponding function for the in
operator is contains
. Here’s how you can use the in
operator and implement the contains
function:
Using the in
operator:
- The object on the right side of the
in
operator is the object on which thecontains
method will be called. - The object on the left side of the
in
operator becomes the argument passed to thecontains
method. - The
in
operator returns a Boolean value indicating whether the object is present in the collection or range.
val list = listOf(1, 2, 3, 4, 5)
val number = 3
val isInList = number in list // Checking if number is in the list
val range = 1..10
val point = 7
val isInRange = point in range // Checking if point is in the range
Implementing the contains
function:
- If you want to use the
in
operator with custom classes, you need to implement thecontains
function in your class. - The
contains
function takes a single parameter that represents the object being checked for membership. - Inside the
contains
function, you can define the logic to determine whether the object is present in the collection or range. - The function should return a Boolean value indicating the result of the membership check.
data class Rectangle(val left: Int, val top: Int, val right: Int, val bottom: Int) {
operator fun Rectangle.contains(p: Point): Boolean {
return p.x in upperLeft.x until lowerRight.x && p.y in upperLeft.y until lowerRight.y
}
}
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect) // true
println(Point(5, 5) in rect) // false
In the Rectangle
class, the contains
function checks if a given Point
falls within the range of the rectangle. By using the open range (until
), the condition point.x in left until right
ensures that the x
coordinate of the point is greater than or equal to left
but less than right
. Similarly, the condition point.y in top until bottom
checks if the y
coordinate of the point is greater than or equal to top
but less than bottom
.
In the implementation of Rectangle .contains, you used the until standard library function to build an open range and then you use the in operator on a range to check that a point belongs to it.
An open range is a range that doesn’t include its ending point. For example, if you build a regular (closed) range using 10..20, this range includes all numbers from 10 to 20, including 20. An open range 10 until 20 includes numbers from 10 to 19 but doesn’t include 20. A rectangle class is usually defined in such a way that its bottom and right coordinates aren’t part of the rectangle, so the use of open ranges is appropriate here.
The rangeTo convention
To create a range in Kotlin, you can use the ..
syntax. For example, 1..10
represents a range that includes all the numbers from 1 to 10. This syntax is a concise way to call the rangeTo
function, which returns a range.
The rangeTo
function is defined in the Kotlin standard library and can be called on any comparable element. It has the following signature:
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
This function returns a ClosedRange
that allows you to check whether different elements belong to it.
Here’s an example of how you can use the rangeTo
function to create a range:
import java.time.LocalDate
fun main() {
val now = LocalDate.now()
val futureRange = now.rangeTo(now.plusDays(10))
for (date in futureRange) {
println(date)
}
}
In this example, now.rangeTo(now.plusDays(10))
creates a range of LocalDate
objects from the current date (now
) to 10 days in the future. The range includes both the starting and ending dates. The rangeTo function isn’t a member of LocalDate but rather is an extension function on Comparable.
When using the rangeTo
operator, it\’s recommended to use parentheses to clearly separate the range expression from other operations. For example:
val range = (start..end).step(2)
Additionally, if you want to call a method on a range, make sure to surround the range expression with parentheses to avoid compilation errors. For example:
(0..n).forEach { print(it) }
By using parentheses, you ensure that the forEach
method is called on the range expression (0..n)
.
The “iterator” convention for the “for” loop
In Kotlin, the for
loop uses the in
operator for iteration. The meaning of the in
operator in this context is different from its use with ranges. Here it is used to perform an iteration over a collection or a range.
When you write a
for
loop likefor (x in list) { ... }
, it is translated into a call to theiterator()
method on thelist
object. This method returns an iterator on which thehasNext()
andnext()
methods are called repeatedly to iterate over the elements of the collection.
The iterator()
method can be defined as an extension function in Kotlin, which explains why you can iterate over a regular Java string. The Kotlin standard library provides an extension function iterator()
on CharSequence
, which is a superclass of String
. It returns a CharIterator
that allows iterating over the characters of the string.
operator fun CharSequence.iterator(): CharIterator<br><br>// for (c in "resume sender app") {}
Here’s an example that demonstrates iterating over a custom range type, ClosedRange<LocalDate>
:
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
object : Iterator<LocalDate> {
var current = start
override fun hasNext() = current <= endInclusive
override fun next() = current.apply { current = plusDays(1) }
}
}
fun main() {
val newYear = LocalDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear
for (dayOff in daysOff) {
println(dayOff)
}
}
// OUTPUT
// 2016-12-31
// 2017-01-01
In this example, the ClosedRange<LocalDate>
type is extended with the iterator()
method, which returns a custom iterator implementation. The iterator starts from the start
date and increments by one day until it reaches the endInclusive
date.
The daysOff
range represents the day before and New Year day. The for
loop then iterates over the dates in the range, printing each day off.
Note how you define the iterator method on a custom range type: you use LocalDate as a type argument. The rangeTo library function, shown in the previous section, returns an instance of ClosedRange, and the iterator extension on ClosedRange allows you to use an instance of the range in a for loop.
By defining the iterator()
extension function on a custom range type, you can use an instance of the range in a for
loop to perform iteration.
Destructuring declarations and component functions
Destructuring declarations in Kotlin allow you to unpack a single composite value and initialize multiple separate variables with its contents. Here’s how it works:
- Destructuring declarations look like regular variable declarations, but with multiple variables grouped in parentheses.
- Under the hood, the principle of conventions is used. To initialize each variable in a destructuring declaration, a function named
componentN
is called, whereN
is the position of the variable in the declaration.
For data classes, the compiler automatically generates
componentN
functions for every property declared in the primary constructor. However, for non-data classes, you need to manually define these functions. Here\’s an example:
class Point(val x: Int, val y: Int) {
operator fun component1() = x
operator fun component2() = y
}
In the example above, the Point
class has two properties, x
and y
. The component1()
function returns the value of x
, and the component2()
function returns the value of y
. These functions enable destructuring declarations to work with instances of the Point
class.
One of the main use cases for destructuring declarations is returning multiple values from a function. You can define a data class to hold the values you want to return and use it as the return type of the function. The destructuring declaration syntax allows you to easily unpack and use the returned values. Let’s take an example of splitting a filename into a name and an extension:
data class NameComponents(val name: String, val extension: String)
fun splitFilename(fullName: String): NameComponents {
val (name, extension) = fullName.split('.', limit = 2)
return NameComponents(name, extension)
}
In the splitFilename
function, the fullName
is split into a list of two elements: the name and the extension. The destructuring declaration val (name, extension) = ...
allows us to directly assign these values to the name
and extension
variables. The function then returns an instance of the NameComponents
data class.
Of course, it’s not possible to define an infinite number of such componentN functions so the syntax would work with an arbitrary number of items, but that wouldn’t be useful, either. The standard library allows you to use this syntax to access the first five elements of a container
If you need to return more than two or three values, Kotlin provides the Pair
and Triple
classes from the standard library. These classes allow you to package multiple values together without defining your own custom class. However, using custom data classes is generally preferred as they provide better clarity about the contained values.
Destructuring declarations and loops
Destructuring declarations can be used not only as top-level statements in functions but also in other places where variable declarations are allowed, such as loops. One useful application is when iterating over entries in a map. Here’s an example that demonstrates this syntax by printing all entries in a given map:
fun printEntries(map: Map<String, String>) {
for ((key, value) in map) {
println("$key -> $value")
}
}
val map = mapOf("resume app" to "Resume Sender App", "kids app" to "ABC123Learn App")
printEntries(map)
// OUTPUT
// resume app -> Resume Sender App
// kids app -> ABC123Learn App
In this example, the printEntries
function takes a Map
as input. The for
loop uses a destructuring declaration (key, value)
to unpack each entry in the map. The key
variable will hold the key of the current entry, and the value
variable will hold the corresponding value. The body of the loop then prints the key-value pair.
Under the hood, Kotlin provides two conventions for achieving this behavior. First, the Kotlin standard library includes an extension function called iterator
on the Map
interface, which returns an iterator over the map entries. This allows you to iterate directly over the map in a loop.
Second, the Map.Entry
interface, representing a key-value pair in the map, has extension functions component1
and component2
. These functions return the key and value, respectively, of the Map.Entry
instance. The destructuring declaration in the loop internally calls these functions to assign the key and value to the declared variables.
In essence, the loop using destructuring declarations is equivalent to the following code:
for (entry in map.entries) {
val key = entry.component1()
val value = entry.component2()
// ...
}
This example highlights the significance of extension functions in Kotlin conventions. The extension functions iterator
, component1
, and component2
enable the concise and readable syntax for iterating over map entries and accessing their key-value pairs.
Conclusion
Conventions in Kotlin provide a powerful way to customize the behavior of language constructs and enhance code readability. By leveraging operator overloading and following function naming conventions, you can create expressive and intuitive APIs. Understanding and utilizing these conventions will help you write clean, concise, and idiomatic Kotlin code.
In this article, we covered various aspects of conventions in Kotlin, including operator overloading conventions for arithmetic operations, comparison operators, indexing, and containment. We also explored Conventions used for collections and ranges and Destructuring declarations.
Remember to practice using conventions in your Kotlin projects to improve code quality and maintainability. Happy coding!