람다를 함수 안에서 정의하면 람다의 파라미터뿐 아니라 람다 앞에서 정의된 함수의 지역 변수까지 람다 안에서 사용할 수 있다.
자바와 다른 점은 final이 아닌 변경 가능한 외부 변수에도 접근 및 변경이 가능하다는 것이다.
예시를 보기 위해 forEach 함수를 사용해보자. forEach는 컬렉션(문자열, 배열도 가능)의 모든 원소에 대해 람다 식을 호출한다. 일반적인 for문과 같지만 좀 더 간결하다.
예시를 살펴보자.
fun countStar(input: String): Int {
var counter = 0
input.forEach {
if (it == '*') counter += 1
}
return counter
}
위 함수는 문자열에서 '*'의 개수를 센다. 람다 밖의 final이 아닌 지역 변수 count에 접근하여 값을 변경한다는 점에 주목하자.
이와 같이 람다 안에서 사용되는 외부 변수를 람다가 포획(capture)한 변수라고 한다.
기본적으로 지역 변수의 생명주기는 함수가 반환되면 끝난다.
하지만 함수가 자신의 로컬 변수를 포획한 람다를 반환하면 함수와 지역 변수의 생명주기가 달라질 수 있다.
즉, 함수가 종료된 후에 반환된 람다 식을 호출하더라도 람다는 포획한 변수에 접근하며 정상 동작한다.
어떻게 이것이 가능할까? 변수를 포획했을 때 어떻게 컴파일 되는지 알아보자.
람다가 final 변수를 포획하면 컴파일 될 때 포획한 변수의 값이 복사되어 람다식 내부에 저장된다. 따라서, 같은 값이지만 실질적으론 다른 변수이다.
var 변수를 포획할 때는 약간의 fake를 쓴다.
변경 가능한 변수 필드를 하나 갖는 특별한 wrapper 클래스를 이용하는데, 그 클래스의 인스턴스를 val로 선언하고 인스턴스의 참조를 람다 내부에 저장한다.
그러면 인스턴스는 final 변수이지만 내부 변수는 변경이 가능하므로 자유롭게 포획 하고 변경도 할 수 있다.
이러한 포획 방식을 코틀린으로 구현해보면 다음과 같다.
class Ref<T>(var value: T)
val counter = Ref(0)
val inc = { counter.value++ }
하지만 개발자가 작성할 때는 wrapper 클래스를 사용하지 않고 아래처럼 변수에 직접 접근하면 된다.
var counter = 0
val inc = { counter++ }