A Deep Dive into Conventions in Kotlin for Streamlined and Intuitive Development

Table of Contents

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:

  1. iterator: By implementing the iterator function, you enable the usage of your class in for loops, similar to how objects implementing java.lang.Iterable work in Java.
  2. invoke: Defining the invoke 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, like myObject(parameter).
  3. compareTo: If you implement the compareTo function, you can use comparison operators (<, <=, >, >=) on instances of your class, allowing for natural ordering.
  4. 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.

Kotlin
data class Point(val x: Int, val y: Int)

You can define the plus function within the Point class as follows:

Kotlin
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 the plus 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:

Kotlin
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:

Kotlin
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:

Overloadable binary arithmetic operators

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

Kotlin
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:

Kotlin
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.

Kotlin
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:

Kotlin
  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:

Kotlin
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:

Kotlin
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 +=.

The += operator can be transformed into either the plus or the plusAssign function call.

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:

Kotlin
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:

  1. Replace the use of the operator with a regular function call that explicitly states the intention. For example, instead of using +=, you can use the add function directly: collection.add(element).
  2. Change a var to a val to make the compound assignment operation inapplicable. If the variable is read-only (val), the compound assignment operator cannot modify its value.
  3. 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 like plusAssign 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 varand not as a val.

Kotlin
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.

Kotlin
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.

Kotlin
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.

Overloadable unary arithmetic operators

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:

Kotlin
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.

An equality check == is transformed into an equals call and a null check

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.

Comparison of two objects is transformed into comparing the result of the compareTo call with zero

Here’s an example of implementing the compareTo function for a Person class, using address book ordering:

Kotlin
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.

Kotlin
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:

Access via square brackets is transformed into a get function call
  • 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 as operator.
  • Inside the get function, you can handle different cases based on the key value and return the appropriate result.
Kotlin
operator fun Point.get(index: Int): Int {
    return when (index) {
        0 -> x
        1 -> y
        else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

Implementing the set operator:

Assignment through square brackets is transformed into a set function call
  • 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 as operator.
  • Inside the set function, you can handle different cases based on the index and update the value accordingly.
Kotlin
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:

Kotlin
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 in operator is transformed into a contains function call
  • The object on the right side of the in operator is the object on which the contains method will be called.
  • The object on the left side of the in operator becomes the argument passed to the contains method.
  • The in operator returns a Boolean value indicating whether the object is present in the collection or range.
Kotlin
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 the contains 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.
Kotlin
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 .. operator is transformed into a rangeTo function call

The rangeTo function is defined in the Kotlin standard library and can be called on any comparable element. It has the following signature:

Kotlin
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:

Kotlin
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:

Kotlin
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:

Kotlin
(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 like for (x in list) { ... }, it is translated into a call to the iterator() method on the list object. This method returns an iterator on which the hasNext() and next() 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.

Kotlin
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>:

Kotlin
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:

  1. Destructuring declarations look like regular variable declarations, but with multiple variables grouped in parentheses.
  2. Under the hood, the principle of conventions is used. To initialize each variable in a destructuring declaration, a function named componentN is called, where N is the position of the variable in the declaration.
Destructuring declarations are transformed into componentN function calls

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:

Kotlin
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:

Kotlin
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:

Kotlin
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:

Kotlin
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!

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!