코틀린의 람다식은 간결한 코드 작성을 돕는 강력한 기능입니다. Java와 유사하지만 몇 가지 차이점과 특수한 기능을 가지고 있습니다.
람다식 기본 문법
람다식은 {매개변수 목록 -> 본문} 형태로 정의됩니다. 코틀린 람다식은 다음과 같은 특징을 가집니다.
- 일반적으로
return문을 허용하지 않습니다. - 단일 매개변수의 경우 암시적 이름
it을 사용할 수 있습니다. 예를 들어,(2 until n).none { n % it == 0 }에서it은2 until n범위의 각 요소를 나타냅니다. - 람다식은 함수에 매개변수로 전달될 수 있습니다. 예를 들어,
fun walkTo(n: Int, action: (Int) -> Unit)함수는Int를 받아Unit(Java의void와 유사)을 반환하는 람다식을 매개변수로 받습니다. - 람다식이 함수의 마지막 인자로 전달될 경우, 함수 호출 괄호 밖으로 빼서 작성할 수 있습니다 (Trailing Lambda). 예:
walkTo(5) { i -> print(i) } - 람다식은 함수의 반환 값으로 사용될 수 있습니다.
- 람다식은 외부 변수를 캡처하는 클로저(Closure) 개념을 가집니다.
클로저 예제 아래 예제는 람다식이 외부 변수를 캡처하는 방식을 보여줍니다.
var multiplier = 2
val numbers = listOf(1, 2)
val doubledList = numbers.map { it * multiplier } // 'multiplier' 값을 캡처
multiplier = 0 // 'multiplier' 값이 변경되지만, 캡처된 값은 변경되지 않음
doubledList.forEach { println(it) } // 출력: 2, 4
위 예제에서 map의 람다식은 multiplier가 2일 때의 값을 캡처합니다. multiplier 값이 이후에 0으로 변경되어도, 이미 캡처된 값은 유지되므로 doubledList의 결과는 2와 4가 됩니다.
람다식의 반환과 태그
코틀린 람다식은 특정 조건에서 return 문을 포함할 수 있습니다. 이는 태그(Label)를 사용하거나 내장(inline) 함수를 통해 가능합니다.
태그를 이용한 반환 태그를 사용하면 람다식 내에서 특정 지점으로 제어를 반환할 수 있습니다. 이는 람다식의 실행을 중단하고 해당 태그가 붙은 지점으로 돌아가게 합니다.
fun processWith(value: Int, action: (Int) -> Unit) {
println("Entering processWith: $value")
action(value)
println("Exiting processWith: $value")
}
fun main() {
(1..3).forEach { i ->
processWith(i) label@{ // 'label@'는 람다식에 태그를 붙임
println("Processing element: $it")
if (it == 2) {
return@label // 'label@' 지점으로 반환 (processWith 함수 내에서 계속 진행)
}
println("Finished element: $it")
}
}
println("End of main function")
}
/*
실행 결과:
Entering processWith: 1
Processing element: 1
Finished element: 1
Exiting processWith: 1
Entering processWith: 2
Processing element: 2
Exiting processWith: 2
Entering processWith: 3
Processing element: 3
Finished element: 3
Exiting processWith: 3
End of main function
*/
위 예제에서 return@label은 it이 2일 때 람다식의 실행을 중단하고 processWith 함수의 다음 코드를 계속 실행하게 합니다. return@processWith를 사용해도 동일한 결과를 얻을 수 있습니다.
람다식의 내장(Inline) 람다식의 내장(inline)은 람다식 코드를 호출 지점에 직접 복사하여 함수 호출 오버헤드를 줄이는 기법입니다. 이는 런타임 시 함수 포인터를 통한 호출 대신 코드 자체를 삽입하는 방식입니다.
내장 람다식과 클로저
내장 람다식은 클로저 동작 방식에 영향을 줄 수 있습니다. 특히 Sequence의 map과 같이 내장된 람다식은 외부 변수의 '주소'를 참조하게 됩니다.
var factor = 2
val sequence = sequenceOf(1, 2)
val multipliedSequence = sequence.map { it * factor } // 'factor'의 주소를 참조
factor = 0 // 'factor' 값이 변경됨
multipliedSequence.forEach { println(it) } // 출력: 0, 0
sequenceOf의 map은 내장 람다식이므로, 람다식 내부의 factor는 외부 factor 변수의 실제 주소를 참조합니다. 따라서 factor가 0으로 변경된 후 forEach를 호출하면 0이 출력됩니다.
내장 함수와 return
내장 함수 내의 람다식에서 return 문을 사용하면, 해당 return은 전체 내장 함수를 종료시키는 효과를 가집니다. 이는 람다식이 호출 지점에 복사되었기 때문에 가능한 동작입니다.
fun processLoop(limit: Int, action: (Int) -> Unit) {
println("Starting loop for $limit")
for (i in 1..limit) {
println("Inside loop iteration: $i")
action(i)
}
println("Loop finished for $limit")
}
fun main() {
processLoop(3) { i ->
println("Processing item $i")
if (i == 2) {
return // 'processLoop' 함수 전체를 종료시킴
}
// ... other operations ...
}
println("End of main after loop attempt")
}
/*
실행 결과:
Starting loop for 3
Inside loop iteration: 1
Processing item 1
Inside loop iteration: 2
Processing item 2
*/
이 예제에서 i가 2가 되면 return 문이 실행되어 processLoop 함수 전체가 즉시 종료됩니다. 이는 람다식이 내장되어 호출 지점에서 실행되기 때문입니다.
내장 함수와 noinline, crossinline
내장 함수에서 람다식을 반환해야 하는 경우, 몇 가지 추가적인 키워드를 사용할 수 있습니다.
noinline
기본적으로 내장 함수의 람다식은 반환될 수 없습니다. 람다식이 함수 포인터가 아닌 코드 조각으로 처리되기 때문입니다. noinline 키워드를 사용하면 해당 람다식을 일반 함수처럼 취급하여 반환할 수 있게 합니다.
inline fun executeAndReturn(value: Int, firstAction: (Int) -> Unit, noinline secondAction: (Int) -> Unit): (Int) -> Unit {
println("Entering executeAndReturn with $value")
firstAction(value)
println("Exiting executeAndReturn with $value")
return secondAction // 'noinline'으로 표시된 람다식은 반환 가능
}
crossinline
crossinline은 inline 함수의 람다식을 반환하면서도, 해당 람다식 내에서 return 문을 사용할 수 없도록 제한하는 키워드입니다. 이는 inline 함수의 이점을 유지하면서도 안전한 반환을 가능하게 합니다. crossinline으로 표시된 람다식 내에서 return을 사용하려고 하면 컴파일 오류가 발생합니다. 반환이 필요한 경우 noinline을 사용하거나, crossinline 람다식을 래핑하여 반환해야 합니다.
inline fun processWithCross(
data: Int,
action1: (Int) -> Unit,
crossinline action2: (Int) -> Unit // 'crossinline' 사용
): (Int) -> Unit {
println("Starting processWithCross for $data")
action1(data)
println("Ending processWithCross for $data")
// action2를 직접 반환할 수 없음. 래핑 필요.
return { input -> action2(input) }
}
fun useCrossInline() {
processWithCross(1,
{ i -> if (i == 1) return@processWithCross else println("Action1: $i") }, // 람다식 내 return (processWithCross 자체를 종료)
{ i ->
// if (i == 2) return // crossinline 람다식 내에서 return 불가 (컴파일 오류)
println("Action2: $i")
}
)
}
// useCrossInline() 호출 시, processWithCross 내부의 action1에서 return@processWithCross가 실행되면
// processWithCross 함수 자체가 종료됩니다.
crossinline은 inline 함수의 장점을 유지하면서도, 람다식의 안전한 반환 또는 실행 흐름 제어를 위해 사용됩니다. return 문이 crossinline 람다식 내에서 사용될 경우, 이는 inline 함수 전체를 종료시키는 것이 아니라 컴파일 오류를 발생시켜 개발자가 의도를 명확히 하도록 유도합니다.