람다식 내에서 외부 변수를 변경하는 것에 대해 아무런 의문을 느끼지 않다가, 문득 궁금증이 생겼습니다. 함수를 작성하는 시점에는 람다식의 블럭 내부가 실행되지 않다가, 호출되는 시점에 실행될텐데 과연 어떻게 동작하게 될까요?
var tabIndex by remember { mutableStateOf(0) }
tabData.forEachIndexed { index, text ->
Tab(
...
onClick = { tabIndex = index },
...
)
}
위 코드는 Jetpack Compose에서 Tab
클릭 시 tabIndex
가 변경되는 예시입니다. tabIndex는 onClick에 할당된 람다식의 외부 변수이지만, 클릭하는 시점에서 tabIndex가 index에 할당됩니다.
코틀린에서 어떻게 람다식에서 외부 변수를 변경시킬 수 있을까요?
자바에 대한 이야기를 또 빼놓고 갈 수가 없습니다
자바에서는 람다식이 final
변수로 선언된 지역 변수에 접근만 가능했고, 변경이 불가능했습니다. 왜냐하면 JVM 메모리 구조 상, 지역변수가 스택에 저장되기 때문입니다. 람다식 바디의 생명주기가 종료되면, 내부의 지역변수 할당을 해제해야 하므로 외부 지역변수의 변경이 가능할 수가 없는 구조입니다.
따라서 자바는 외부 지역변수를 복제한 데이터를 통해 읽기가 가능한 캡쳐링만을 허용했습니다.
코틀린에서는 val
을 포획했는지, var
을 포획했는지에 따라 달라지는 것을 디컴파일 한 결과를 통해 확인할 수 있는데요, 한 번 살펴보겠습니다.
val
을 캡쳐한 경우fun main() {
val immutable = 0
exampleFunction(lambda = { immutable })
}
fun exampleFunction(
lambda: () -> Int
) {
lambda()
}
public final class MainKt {
public static final void main() {
final int immutable = 0;
exampleFunction((Function0)(new Function0() {
public Object invoke() {
return this.invoke();
}
public final int invoke() {
return immutable;
}
}));
}
public static void main(String[] var0) {
main();
}
public static final void exampleFunction(@NotNull Function0 lambda) {
Intrinsics.checkNotNullParameter(lambda, "lambda");
lambda.invoke();
}
}
val
의 경우, 자바와 마찬가지로 final
로 선언되어 해당 값이 캡쳐되는 것을 확인할 수 있습니다.
var
을 캡쳐한 경우fun main() {
var mutable = 0
exampleFunction(lambda = { mutable++ })
}
fun exampleFunction(
lambda: () -> Int
) {
lambda()
}
public final class MainKt {
public static final void main() {
final Ref.IntRef mutable = new Ref.IntRef();
mutable.element = 0;
exampleFunction((Function0)(new Function0() {
public Object invoke() {
return this.invoke();
}
public final int invoke() {
Ref.IntRef var10000 = mutable;
int var1;
var10000.element = (var1 = var10000.element) + 1;
return var1;
}
}));
}
public static void main(String[] var0) {
main();
}
public static final void exampleFunction(@NotNull Function0 lambda) {
Intrinsics.checkNotNullParameter(lambda, "lambda");
lambda.invoke();
}
}
보이시나요? IntRef
라는 final
클래스에 의해 래핑되고, 내부적으로 변경이 가능한 변수로 포획한다는 것을 확인할 수 있습니다. IntRef
로 생성된 클래스는 JVM 힙에 할당 될 것이기 때문에 람다 블록에서 변수를 변경하고 생명주기가 끝나도 영향을 받지 않을 수 있습니다.
public static final class IntRef implements Serializable {
public int element;
@Override
public String toString() {
return String.valueOf(element);
}
}
일어나세요 용사여