[Kotlin] 코틀린은 람다식에서 외부 변수를 어떻게 참조할까? [람다 캡쳐링] [Lambda Capturing]

Choi Sang Rok·2023년 1월 5일
6
post-thumbnail

람다식 내에서 외부 변수를 변경하는 것에 대해 아무런 의문을 느끼지 않다가, 문득 궁금증이 생겼습니다. 함수를 작성하는 시점에는 람다식의 블럭 내부가 실행되지 않다가, 호출되는 시점에 실행될텐데 과연 어떻게 동작하게 될까요?


람다 캡쳐링 📸

  • 람다식 외부에서 정의한 변수를 참조하는 것
var tabIndex by remember { mutableStateOf(0) }
tabData.forEachIndexed { index, text ->
    Tab(
        ...
        onClick = { tabIndex = index },
        ...
    )
}

위 코드는 Jetpack Compose에서 Tab 클릭 시 tabIndex가 변경되는 예시입니다. tabIndex는 onClick에 할당된 람다식의 외부 변수이지만, 클릭하는 시점에서 tabIndex가 index에 할당됩니다.
코틀린에서 어떻게 람다식에서 외부 변수를 변경시킬 수 있을까요?


In JAVA ☕

자바에 대한 이야기를 또 빼놓고 갈 수가 없습니다

자바에서는 람다식이 final 변수로 선언된 지역 변수에 접근만 가능했고, 변경이 불가능했습니다. 왜냐하면 JVM 메모리 구조 상, 지역변수가 스택에 저장되기 때문입니다. 람다식 바디의 생명주기가 종료되면, 내부의 지역변수 할당을 해제해야 하므로 외부 지역변수의 변경이 가능할 수가 없는 구조입니다.

따라서 자바는 외부 지역변수를 복제한 데이터를 통해 읽기가 가능한 캡쳐링만을 허용했습니다.


In Kotlin 🎯

코틀린에서는 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);
        }
    }
profile
android_developer

4개의 댓글

comment-user-thumbnail
2023년 1월 6일

일어나세요 용사여

답글 달기
comment-user-thumbnail
2023년 1월 6일

코틀린 말고 입틀린은 없나요?

답글 달기
comment-user-thumbnail
2023년 1월 6일

람다 말고 람각은 없나요?

답글 달기
comment-user-thumbnail
2023년 11월 14일

좋은 글 감사합니다~

답글 달기