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:
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,
outerFunction
declares a variablecounter
initialized to0
.- It then returns a lambda that prints the current value of
counter
and increments it. - When we invoke
lambda()
, it prints the current value and increases it, demonstrating thatcounter
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.
@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.
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:
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 aCounter
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:
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 affectcounter
. - 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.