1. Overview

In Kotlin, each variable declared in a scope will shadow other variables with the same name that are outside that scope. This applies to all scope levels, such as within functions, classes, or block code.

Sometimes, variable shadowing is useful — for example, for separating our code from other parts of the system. However, this could be confusing for programmers who are not familiar with it. Although Kotlin allows this, it strongly recommends avoiding the use of shadowing as much as possible. If we use an IDE like IntelliJ IDEA, we will easily find warnings highlighted for us.

In this tutorial, we’ll discuss some situations where shadowing can occur in Kotlin.

2. Class Member Variable Shadowing

This situation happens when we have a member variable of a class, and then we create another variable with the same name within a function scope or code block:

class Car {
    val speed: Int = 100

    fun upSpeed() : Int {
        val speed = speed * 2
        return speed
    }
}

assertEquals(100, Car().speed)
assertEquals(200, Car().upSpeed())

For example, the Car class has a speed property with an initial value of 100. Then, we have a function upSpeed() that doubles the local speed value in the function but does not change the speed property value of the Car object.

The local variable speed in the upSpeed() function causes the local variable to cover (shadow) the speed variable, which is a property of the Car class. As a result, when using speed in the upSpeed() function, it is the local variable that is accessed, not the speed property of the Car object.

To avoid shadowing, we can use the variable name newSpeed in the upSpeed() function:

fun upSpeed() : Int { 
    val newSpeed = speed * 2 // Using a new variable name to avoid shadowing
    return newSpeed
}

Unless we actually want to access speed in the Car class, in which case we should use the this keyword for clarity:

fun upSpeed() : Int {
    return this.speed * 2
}

Here, we access the speed field using the this keyword.

3. Parameter Shadowing

Parameter shadowing can occur when we declare a function parameter with the same name as a variable or parameter from an enclosing scope:

fun calculateTotalPrice(discount: Int) {
    val discount = discount + 10 // Shadowing the parameter 'discount'
    // ...
}

In the function, we have a local discount variable whose value is the result of adding 10 to the received discount parameter value. This causes the discount parameter to be shadowed by a local variable with the same name.

This can cause confusion and potential errors if we’re not careful. We should avoid shadowing in cases like this:

fun calculateTotalPrice(discount: Int) {
    val updatedDiscount = discount + 10 // Using a new variable name to avoid shadowing
    // ...
}

It’s a good idea to use different variable names to prevent potential errors and make the code easier to understand.

4. Local Variable Shadowing

In Kotlin, each variable declared in a scope will shadow other variables with the same name that are outside that scope. So in nested functions, it will also occur:

val price = 100 // local variable
val discountRate = 0.1

fun applyDiscount(price: Int): Double { // Nested function with parameter named 'price'
    val discountRate = 0.2  // shadowing the outer variable discountRate
    return price * (1 - discountRate) // 'price' here refers to the parameter, not the outer variable
}

assertEquals(80.0, applyDiscount(price))

The applyDiscount() nested function has a local variable named discountRate with value 0.2. This shadows the outer discountRate variable (with value 0.1) within the scope of the nested function.

To avoid shadowing, simply choose a different name for the discount rate variable within applyDiscount(). For example, let’s use innerDiscountRate:

fun applyDiscount(price: Int): Double {
    val innerDiscountRate = 0.2 // Use a different name to avoid shadowing
    return price * (1 - innerDiscountRate)
}

If the inner function truly needs the discount rate from the outer scope, we can access it directly without introducing a separate variable:

fun applyDiscount(price: Int): Double {
    return price * (1 - discountRate) // Use the outer discountRate directly
}

This looks reasonable and reduces confusion.

5. Loop Variable Shadowing

Variable shadowing can also occur in loops when we declare a variable as an iterator in the loop that has the same name as a variable outside the loop:

val numbers = listOf(1, 2, 3, 4, 5)
for (number in numbers) {
    val number = number * 2 // Shadowing the loop variable 'number'
    // ...
}

For example, inside the loop, we have local variable number, which overwrites the loop variable with the same name. In this case, the local variable number experiences shadowing of the loop variable.

This can cause confusion, too. It would be better if we changed it to something like:

for (number in numbers) {
    val newNumber = number * 2
    // ...
}

We use the newNumber variable to store the result of the multiplication, avoiding the shadowing issue.

6. Shadowing Extension Functions

In Kotlin, we can add extensions to built-in data types and give them the same name as the built-in function. However, the extension function will be called based on the context:

val numbers = listOf(1, 2, 3, 4, 5)
// ...
assertEquals(15, numbers.sum())

fun List<Int>.sum(): Int { // shadowing built-in function sum()
    var sum = 0
    this.forEach { sum += it * 2 }
    return sum
}

assertEquals(30, numbers.sum())

So, if we call sum() on a List<Int> object, it will call the extension function we defined, not the built-in Kotlin function sum().

This has a similar effect to shadowing, so we must be careful when using this approach.

To avoid shadowing, we can use another name:

fun List<Int>.sumByTwo(): Int { // Rename to avoid shadowing
  var sum = 0
  this.forEach { sum += it * 2 }
  return sum
}

Or, we can directly use the sumOf() function and then modify it inside the lambda:

val doubledSum = numbers.sumOf { it * 2 } // Modify lambda in sumOf
assertEquals(30, doubledSum)

Yes, we’ve reduced the confusion, and our code looks simpler and more expressive.

7. Variable Shadowing In Lambda

Lambda expressions are essentially anonymous functions that we can treat as values. So, lambda has its own scope so that variables declared in the lambda can only be accessed from within the lambda. However, lambda can access variables outside the scope, which leads to the possibility of shadowing:

val number = 10
val lambda = { number : Int ->
    val number = 1// Local variable in lambda
}

Parameters received by the lambda are also considered to be within its local scope. These parameters can be accessed and referenced within the lambda body. So, this also allows parameter shadowing to occur in lambda:

val numbers = listOf(1, 2, 3, 4, 5)
// ...
var sum = 0

numbers.forEach { number ->
    val number = 0 // shadowing value
    sum += number
}

assertEquals(0, sum)

For example, in each iteration, we have local variable number, which overwrites the variable number of the lambda parameter, resulting in shadowing.

To avoid shadowing, we need to look at the context. If the goal is to sum all elements, then we need to directly add the current element to the sum:

numbers.forEach { number ->
    sum += number // Directly access the current element in the loop
}

In this case, each element is accessed directly in the loop and added to the sum variable.

8. Top-Level Function Variable Shadowing

In Kotlin, we can write code without creating classes, so variable shadowing can also occur with top-level functions:

val number = 10 // Top-level variable

fun upNumber() : Int { // top-level function
    val number = 20 // shadowing top-level variable
    return number
}

assertEquals(20, upNumber())
assertEquals(10, number)

This code defines a top-level number variable with a value of 10. Then, we have an upNumber() function that also defines a local variable with the name number and value 20, which causes shadowing of the top-level number variable.

The solution is similar to other shadowing cases, but in this case, the top-level variable cannot be accessed with the this keyword.

9. Conclusion

In this article, we discussed some situations that allow variable shadowing in Kotlin. While sometimes slightly useful, it often makes the code harder to read, introduces potential logic errors, and is harder to debug.

To avoid its disadvantages, shadowing should usually be avoided. We can rely on our IDE warnings to help us keep an eye on it.

We also offered possible solutions, such as using different variable names or using the this keyword if possible.

As always, code samples can be found over on GitHub.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments