Kotlin is known for its concise syntax, null safety, and powerful type system. Among its notable features is the concept of smart casts, which allows developers to write more efficient and type-safe code by automatically casting a variable to a more specific type after certain conditions are met.
What are Smart Casts?
Smart casts are automatic type conversions performed by the Kotlin compiler. When you check a variable’s type using is or perform a safe call on a nullable type, the compiler intelligently casts the variable to the more specific type within the scope where the type check is valid. This eliminates the need for explicit casts and reduces boilerplate code.
Why Use Smart Casts?
- Reduces Boilerplate: Eliminates the need for explicit casts, making code cleaner and more readable.
- Improves Type Safety: Ensures type conversions are safe by performing checks at compile time.
- Enhances Readability: Makes code easier to understand by reducing noise and focusing on the logic.
How to Use Smart Casts in Kotlin
Here are several examples demonstrating the usage of smart casts in Kotlin.
Example 1: Using is Type Checks
When you check the type of a variable using is, Kotlin smart casts the variable to that type within the if block.
fun processInput(input: Any) {
if (input is String) {
// 'input' is automatically cast to 'String' within this block
println("Input length: ${input.length}")
} else if (input is Int) {
// 'input' is automatically cast to 'Int' within this block
println("Input doubled: ${input * 2}")
} else {
println("Unknown input type")
}
}
fun main() {
processInput("Hello Kotlin") // Output: Input length: 13
processInput(42) // Output: Input doubled: 84
processInput(true) // Output: Unknown input type
}
In this example, within the if blocks, input is automatically cast to String and Int respectively, allowing you to access their specific properties and methods without explicit casting.
Example 2: Using !is Type Checks
Similarly, if you check that a variable is not of a certain type using !is, Kotlin smart casts the variable to the complementary type within the corresponding block.
fun processNonString(input: Any) {
if (input !is String) {
// 'input' is automatically treated as 'Any' but not 'String' within this block
println("Input is not a string: $input")
} else {
println("Input is a string: ${input.length}")
}
}
fun main() {
processNonString(123) // Output: Input is not a string: 123
processNonString("Kotlin") // Output: Input is a string: 6
}
Here, within the if block, input is known not to be a String, so you can handle it accordingly.
Example 3: Smart Casts with Nullable Types
Kotlin smart casts also work with nullable types when combined with null checks.
fun processNullableString(input: String?) {
if (input != null) {
// 'input' is automatically cast to 'String' (non-nullable) within this block
println("Input length: ${input.length}")
} else {
println("Input is null")
}
}
fun main() {
processNullableString("Kotlin") // Output: Input length: 6
processNullableString(null) // Output: Input is null
}
In this example, within the if block, input is smart cast to a non-nullable String, so you can safely access its length property without using the safe call operator (?.).
Example 4: Smart Casts with When Expressions
Smart casts can be effectively used with when expressions, making complex type checking and handling more readable.
fun describe(obj: Any): String {
return when (obj) {
is String -> "A string of length ${obj.length}"
is Int -> "An integer with value ${obj * 2}"
is List<*> -> "A list with ${obj.size} elements"
else -> "Unknown type"
}
}
fun main() {
println(describe("Kotlin")) // Output: A string of length 6
println(describe(10)) // Output: An integer with value 20
println(describe(listOf(1, 2, 3))) // Output: A list with 3 elements
println(describe(true)) // Output: Unknown type
}
In this when expression, the variable obj is smart cast to String, Int, and List<*> within their respective branches.
Example 5: Smart Casts and Immutability
Smart casts require the variable to be immutable (i.e., declared with val) to guarantee type safety. If the variable is mutable (declared with var), the compiler cannot guarantee that the type hasn’t changed between the type check and the usage, so smart casts are not applied.
fun checkStringLength(input: Any) {
if (input is String) {
// 'input' is smart cast to String here
println("Length is ${input.length}")
}
}
fun main() {
val immutableInput: Any = "Kotlin"
if (immutableInput is String) {
// Smart cast applied
println("Length of immutableInput is ${immutableInput.length}")
}
var mutableInput: Any = "Kotlin"
if (mutableInput is String) {
// Smart cast NOT applied, you need to cast explicitly
println("Length of mutableInput is ${(mutableInput as String).length}")
}
}
In this example, immutableInput is smart cast, while mutableInput requires an explicit cast because its type could potentially change after the type check.
Limitations of Smart Casts
While smart casts are powerful, they have certain limitations:
- Immutability Requirement: Smart casts are only guaranteed to work with immutable variables (declared with
val). If a variable is mutable (var), the compiler may not apply smart casts to ensure type safety. - Visibility: Smart casts are scope-dependent. They are valid only within the block or branch where the type check is performed.
Conclusion
Kotlin’s smart casts provide a convenient and efficient way to work with different types while maintaining type safety. By automatically casting variables based on type checks, smart casts reduce boilerplate code, improve readability, and enhance the overall development experience. Understanding and leveraging smart casts can lead to cleaner, more maintainable Kotlin code.