kotlin lambda and capture value

이태훈·2022년 2월 17일
0

오늘은.. 코틀린에서의 람다식과 람다식에서 variable을 capture하는 방식이 어떻게 이루어지는 지 알아보겠습니다.

먼저 람다식에 대해 알아보면, 프로그래밍 언어에서 익명함수를 지칭하는 용어로 주로 고차 함수에 인자(argument)로 전달되거나 고차 함수가 돌려주는 결과값으로 쓰입니다.

코틀린에서 람다식을 선언할 때는 다음과 같이 선언할 수 있습니다.

val lambda = { x: Int, y: Int -> x + y }
val lambda = fun(x: Int, y: Int) = x + y

자 그러면 이러한 람다식이 자바로 어떻게 변환되는 지 알아보겠습니다.

먼저, variable이 capture 되었다는 뜻을 이해하기 위해 짚고 가겠습니다. Closure 밖의 변수를 Closure 안에서 사용하게 되면 그 변수가 capture 되었다고 합니다.

이해하기 쉽도록 코드를 보겠습니다.

val lambdaWithoutCapture = { 1 }

var mutableValue = 2
val lambdaWithMutableCaptureValue = {
    ++mutableValue + 1
}

첫 번째 람다식은 closure 밖의 변수를 사용하지 않았기 때문에 captured variable이 없고, 두 번째 람다식은 closure 밖의 변수를 사용하고 있기 때문에 captured variable이 있습니다.

그런데 또 하나.. 자바에서는 kotlin의 var keyword와 같은 mutable한 변수를 capture 하는 것을 금지하고 있습니다.

위 사진처럼 빨간줄이 뜹니다.. 하지만, 코틀린에서는 어떻게 mutable한 변수를 사용하고, 값도 바꿀 수 있을까요?

코틀린에서는 mutable한 변수를 사용하기 위해 Ref라는 클래스를 래핑하여 씁니다.

mutable variable을 사용한 람다식을 디컴파일 해보면

Ref.IntRef mutableValue = new Ref.IntRef();
mutableValue.element = 2;
Function0<Integer> lambdaWithMutableCaptureValue = new MainKt$main$lambdaWithMutableCaptureValue$1(mutableValue);

mutableValue를 IntRef 클래스로 wrapping해서 사용하고 있습니다.

그럼 이제, kotlin 람다식이 java코드로 어떻게 변하는 지 알아보겠습니다.

람다식이 변환되는 케이스는 다음과 같이 있습니다.

  • Captured Variable을 가지고 있지 않을 때
  • Captured Variable을 가지고 있을 때
  • inline keyword marked

아래와 같은 함수를 사용하여 디컴파일을 해보겠습니다.

fun basicFunction(block: () -> Int): Int {
    return block()
}

inline fun inlinedFunction(block: () -> Int): Int {
    return block()
}

먼저 Captured Variable을 가지고 있지 않을 때를 보겠습니다.

basicFunction {
    1
}

inlinedFunction {
    1
}
MainKt.basicFunction(LambdaTestKt$main$1.INSTANCE);

int $i$f$inlinedFunction = 0;
int $i$a$-inlinedFunction-LambdaTestKt$main$2 = 0;

static final class LambdaTestKt$main$1 extends Lambda implements Function0<Integer> {
    public static final LambdaTestKt$main$1 INSTANCE = new LambdaTestKt$main$1();
    
    public final int invoke() {
      return 1;
    }
    
    LambdaTestKt$main$1() {
      super(0);
    }
  }

inline keyword를 적용하지 않은 lambda 식은 singleton class로 변환된 것을 확인할 수 있습니다.

이렇게 생성된 singleton class은 해당 lambda식이 다시 쓰일 때마다 재사용됩니다.

반면에 inline keyword가 적용된 lambda식은 별 다른 게 없네요

다음 경우를 보겠습니다.

var mutableValue = 2

basicFunction {
    ++mutableValue + 1
}

inlinedFunction {
	++mutableValue + 1
}

Mutable Variable을 capture하는 람다식입니다.

Ref.IntRef mutableValue = new Ref.IntRef();
mutableValue.element = 2;
MainKt.basicFunction(new LambdaTestKt$main$1(mutableValue));
int $i$f$inlinedFunction = 0;
int $i$a$-inlinedFunction-LambdaTestKt$main$2 = 0;
mutableValue.element++;
mutableValue.element + 1;

static final class LambdaTestKt$main$1 extends Lambda implements Function0<Integer> {
    public final int invoke() {
      this.$mutableValue.element++;
      return this.$mutableValue.element + 1;
    }
    
    LambdaTestKt$main$1(Ref.IntRef $mutableValue) {
      super(0);
	}
}

Mutable variable을 caputre하여 IntRef 클래스로 wrapping된 것을 먼저 볼 수 있습니다.
그리고 inline keyword가 적용되지 않은 lambda식의 경우 새 객체가 만들어지는 것을 볼 수 있습니다.
이렇게 captured variable을 가진 lambda 식은 매 호출마다 새로운 객체가 생성됩니다.

inline keyword가 적용된 lambda 식은 아무 객체나 클래스가 생성되지 않습니다.

다음으로 남은 경우인 captured variable을 가지고 있지 않은 경우도 Ref 클랙스로 wrapping 하지 않는 다는 것만 빼면 똑같이 변환됩니다.

profile
https://www.linkedin.com/in/%ED%83%9C%ED%9B%88-%EC%9D%B4-7b9563237

0개의 댓글