Kotlin – 23 – Smart Casts in Kotlin

Smart casts are a powerful and convenient feature in Kotlin that automatically cast a variable to a more specific type when certain conditions are met. They simplify type handling, enhance code readability, and reduce the need for explicit type checks and casts. In this guide, we’ll explore smart casts in Kotlin, how they work, and practical examples of their usage.

Introduction to Smart Casts

Smart casts are a form of automatic type casting provided by the Kotlin compiler. They are designed to address common scenarios where developers need to check the type of a variable and then cast it to that type. Instead of forcing developers to perform these checks and casts manually, Kotlin’s smart casts handle them automatically when it’s safe to do so.

Safe Type Checks (is)

In Kotlin, you can use the is operator to check if an object is an instance of a particular type. When such a check is successful, the Kotlin compiler smartly casts the object to that type within the scope of the if or when block. Here’s an example:

fun processStringOrInt(value: Any) {
    if (value is String) {
        // Inside this block, 'value' is automatically cast to 'String'
        println("Length of the string: ${value.length}")
    } else if (value is Int) {
        // Inside this block, 'value' is automatically cast to 'Int'
        println("Square of the integer: ${value * value}")
    }
}

In this code, the is operator is used to check whether value is a String or an Int. Within each if block, the compiler smartly casts value to the appropriate type, enabling you to access type-specific members or perform type-specific operations without additional casts.

When Expression and Smart Casts

Smart casts are especially useful when combined with Kotlin’s when expression, which allows for concise and expressive branching based on types. Here’s an example:

fun processValue(value: Any) {
    when (value) {
        is String -> {
            // 'value' is automatically cast to 'String' here
            println("Processing a string: $value")
            // You can access 'String'-specific members
            val length = value.length
            println("Length: $length")
        }
        is Int -> {
            // 'value' is automatically cast to 'Int' here
            println("Processing an integer: $value")
            // You can perform 'Int'-specific operations
            val square = value * value
            println("Square: $square")
        }
        else -> {
            println("Unknown type: $value")
        }
    }
}

In this example, the when expression allows you to handle different types of value cleanly and safely. Within each branch of the when, smart casts make sure that value is cast to the expected type, eliminating the need for explicit casting or type checks.

Custom Smart Cast Predicates

Kotlin’s smart casts are not limited to built-in types; you can also create custom smart cast predicates by defining functions that return true or false based on your custom logic. When the compiler recognizes such predicates, it performs smart casts accordingly. Here’s an example:

fun canBeCastedToRectangle(shape: Any): Boolean {
    return shape is Rectangle || shape is Square
}

fun processShape(shape: Any) {
    if (canBeCastedToRectangle(shape)) {
        // Inside this block, 'shape' is smartly cast to 'Rectangle'
        println("Processing a rectangle or square")
        // You can access 'Rectangle'-specific members
        val area = (shape as Rectangle).calculateArea()
        println("Area: $area")
    } else {
        println("Cannot process this shape")
    }
}

In this code, the canBeCastedToRectangle function serves as a custom smart cast predicate. When processShape is called, it checks if shape can be cast to a Rectangle or Square. If so, it safely casts shape to Rectangle within the if block, where you can access Rectangle-specific members.

Advantages of Smart Casts

Smart casts in Kotlin offer several benefits:

  1. Safety: Smart casts reduce the risk of runtime errors by ensuring that variables are cast to the correct type only when type conditions are met.
  2. Conciseness: Smart casts eliminate the need for explicit type checks and casts, resulting in cleaner and more concise code.
  3. Readability: Code that uses smart casts is more readable and expressive, as it clearly conveys the intent without the noise of type checks and casts.
  4. Less Boilerplate: Developers can focus on business logic and reduce the boilerplate code associated with manual type checks and casting.
  5. Improved Code Quality: Smart casts contribute to more robust code by preventing type-related bugs and improving code maintainability.
Command and Example

Here’s a complete example illustrating the use of smart casts in Kotlin:

open class Shape

class Rectangle(val width: Double, val height: Double) : Shape() {
    fun calculateArea() = width * height
}

class Square(val sideLength: Double) : Shape() {
    fun calculateArea() = sideLength * sideLength
}

fun main() {
    val rectangle: Shape = Rectangle(5.0, 3.0)

    if (rectangle is Rectangle) {
        // Inside this block, 'rectangle' is smartly cast to 'Rectangle'
        println("Processing a rectangle")
        val area = rectangle.calculateArea()
        println("Area: $area")
    } else if (rectangle is Square) {
        // Inside this block, 'rectangle' is smartly cast to 'Square'
        println("Processing a square")
        val area = rectangle.calculateArea()
        println("Area: $area")
    } else {
        println("Unknown shape")
    }
}

In this example, we define a hierarchy of shapes and use smart casts to handle different shapes within the main function. Smart casts ensure that rectangle is correctly cast to its actual type (Rectangle or Square) within the respective branches of the if block, enabling type-specific operations without manual type checks or casts. This results in cleaner and safer code.