Workarounds for Capturing Non-Final Variables in Kotlin Lambdas

Table of Contents

Kotlin lambdas are powerful, but they come with a constraint: they can only capture final (effectively immutable) variables from their enclosing scope. This can be a challenge when you need to modify a variable inside a lambda.

In this blog, we will explore why this restriction exists and the workarounds you can use to capture non-final variables in Kotlin lambdas.

What Happens When Capturing Non-Final Variables in Kotlin Lambdas?

In Kotlin, when a lambda captures a non-final variable (i.e., a var variable), the variable is essentially wrapped in an internal object. This allows the lambda to modify the value of the variable even after it has been captured. This behavior is different from languages like Java, where lambda expressions can only capture final or effectively final variables.

Let’s look at a simple example where a lambda captures a non-final variable:

Kotlin
fun outerFunction(): () -> Unit {
    var counter = 0
    return { println(counter++) }
}

// Place the following code inside main()

val lambda = outerFunction()
lambda() // prints "0"
lambda() // prints "1"

Here,

  1. outerFunction declares a variable counter initialized to 0.
  2. It then returns a lambda that prints the current value of counter and increments it.
  3. When we invoke lambda(), it prints the current value and increases it, demonstrating that counter retains its state across multiple lambda executions.

This behavior occurs because the variable counter is wrapped in an object that allows its modification even after being captured by the lambda.

You won’t believe this..! Wait a minute—just check the Java bytecode for outerFunction(), and you’ll see.

Java
@NotNull
public static final Function0 outerFunction() {
    final Ref.IntRef counter = new Ref.IntRef();
    counter.element = 0;
    // ...
}

So, what is Ref.IntRef? Is it a mutable wrapper object?

Yes, Ref.IntRef is a mutable wrapper object used by the Kotlin compiler to allow lambdas to capture and modify integer values.

Actually, Ref.IntRef is an internal class in Kotlin’s standard library (kotlin.jvm.internal package). It is used when lambdas capture a mutable var of type Int because primitive types (int) cannot be directly captured by lambdas in Java due to Java’s pass-by-value nature.

This wrapper enables mutability, meaning that changes made inside the lambda affect the original variable.

Note- Kotlin provides similar wrappers for other primitive types also.

Now, What Happens When You Try to Modify a Captured Variable from Outside?

While lambdas can modify captured variables, you cannot modify those variables from outside the lambda. If you try to do so, the Kotlin compiler will raise an error.

Kotlin
fun outerFunction(): () -> Unit {
    var counter = 0
    return { println(counter++) }
}

val lambda = outerFunction()
lambda.counter = 10 // Compilation error: "Unresolved reference: counter"

Why Does This Happen?

The reason this code fails is that counter is wrapped inside an object, and the lambda is the only one with access to that object. Attempting to modify counter from outside the lambda results in a compilation error since counter is not a property of lambda itself.

Workaround (Recommended): Use an Explicitly Mutable Object

If you need to modify a captured variable externally, one approach is to use a mutable wrapper object. For example:

Kotlin
class Counter(var value: Int)

fun outerFunction(counter: Counter): () -> Unit {
    return { println(counter.value++) }
}

fun main() {
    val counter = Counter(0)
    val lambda = outerFunction(counter)

    lambda() // Prints "0"
    lambda() // Prints "1"

    counter.value = 10 // Successfully modifies the counter
    println(counter.value) // Prints "10"
}

Here,

  • Instead of a simple var counter, we use a Counter class to hold the value.
  • The lambda captures an instance of Counter, allowing external modification.
  • Now, counter.value can be updated externally without compiler errors.

Alternative (Just for Understanding): Using a Local Variable Inside the Lambda

Another approach is to declare a new local variable inside the lambda itself. However, note that this does not allow external modification of the original captured variable:

Kotlin
fun outerFunction(): () -> Unit {
    var counter = 0
    return {
        val newCounter = 10
        println(newCounter)
    }
}

fun main() {
    val lambda = outerFunction()
    lambda() // Prints "10"
}

Here,

  • The newCounter variable exists only within the lambda and does not affect counter.
  • This is useful when you need a temporary, independent variable inside the lambda.

Note: This is just for the sake of understanding; it’s not recommended.

Want more details? Check out the full guide: [Main Article URL]

Conclusion

Capturing non-final variables in Kotlin lambdas provides flexibility, but it also requires an understanding of how Kotlin wraps these variables internally. While lambdas can modify captured variables, external modification is not allowed unless an explicit wrapper object is used. By following best practices, you can ensure safe and maintainable code when working with non-final variable captures in Kotlin.

By mastering these concepts, you’ll be better equipped to leverage Kotlin’s powerful functional programming features while writing efficient and robust code.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!