devseop08 님의 블로그
[API] 3. 고차 함수 본문
고차 함수: 람다를 파라미터 반환값으로 사용
10.1. 다른 함수를 인자로 받거나 반환하는 함수 정의: 고차 함수
- 고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수다.
- 코틀린에서는 람다나 함수 참조를 사용해 함수를 값으로 표현할 수 있다.
- 고차 함수를 사용하는 법에 더해 이제는 고차 함수를 정의하는 방법을 살펴본다.
- 고차 함수를 정의하려면 함수 타입을 먼저 알아야 한다.
10.1.1 함수 타입은 람다의 파라미터 타입과 반환 타입을 지정한다.
- 람다를 인자로 받는 함수를 정의하려면 먼저 람다 파라미터의 타입을 어떻게 선언할 수 있는지 알아야 한다. (코틀린 타입 중 함수 타입이라는 것이 있고, 이 함수 타입을 어떻게 선언할 수 있는지 알아야 한다. )
- 파라미터 타입을 정의하기 전에 더 단순한 경우로 람다를 로컬 변수에 대입하는 경우를 보자.
- 코틀린 타입 추론에 의해 변수의 타입을 지정하지 않고도 람다를 변수에 대입할 수 있다.
val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }- 컴파일러는 sum과 action이 함수 타입임을 추론한다.
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
val action: () -> Unit = { println(42) }- 함수 타입 문법(함수 타입 지정 형식)
(Int, String) -> UnitUnit 타입은 의미있는 값을 반환하지 않는 함수 반환 타입에 쓰는 특별한 타입인데 함수의 반환 타입으로 쓰일 때는 생략이 가능했지만 함수 타입을 선언할 때는 반드시 Unit을 명시해줘야 한다.
함수 타입 변수에 대입되는 람다식 안에서
{ x, y -> x + y}처럼 x와 y의 타입을 생략해도 된다.변수 선언의 일부분인 함수 타입 안에 파라미터 타입을 지정했기 때문에 람다 자체에서는 파라미터 타입을 굳이 지정할 필요가 없다.
함수 타입과 널
다른 함수와 마찬가지로 함수 타입에서도 반환 타입을 널이 될 수 있는 타입으로 지정 가능
var canReturnNull: (Int, Int) -> Int? = { x, y -> null }함수 타입 변수 자체를 널이 될 수 있는 타입으로 선언할 수도 있다.
var funOrNull: ((Int, Int) -> Int)? = null
10.1.2 인자로 전달 받은 함수 호출
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3) // 인자로 전달 받은 함수 호출
println("The result is $result")
}
fun main() {
twoAndThree{ a, b -> a + b } // 람다(식)를 인자로 전달
// The result is 5
twoAndThree{ a, b -> a * b } // 람다(식)를 인자로 전달
// The result is 6
}- String에 대한 filter 확장 함수 선언: 함수 타입 파라미터 선언 이용
- 술어 함수(람다)를 파라미터 받는 filter 함수 정의
fun String.filter(predicate: (Char) -> Boolean): Stringfun String.filter(predicate: (Char) -> Boolean) : String {
return buildString{
for(char in this@filter){ this: StringBuilder가 아닌 String
if(predicate(char)) append(char) // this: StringBuilder, this.append
}
}
}
fun main() {
println("ab1c".filter{ it in 'a'..'z' })
// abc
}확장 함수와 buildString 함수 모두 수신 객체를 정의하기 때문에 this 앞에 레이블을 붙여 buildString 람다의 수신 객체가 아니라 filter의 바깥쪽 수신 객체(입력 문자열)에 접근해야 한다.
10.1.3 자바에서 코틀린 함수 타입 사용
자동 SAM 변환을 통해 코틀린 람다를 함수형 인터페이스를 요구하는 자바 메서드에 넘길 수 있다.
함수 타입을 사용하는 코틀린 코드도 자바에서 쉽게 호출 가능
/* 코틀린 선언 */
fun processTheAnswer(f: (Int) -> Int)) {
println(f(42))
}
/* 자바 호출 */
processTheAnswer(n -> n + 1);
// 43- Unit을 반환하는 함수나 람다를 자바로 작성할 수도 있지만 코틀린 Unit 타입에는 값이 존재하므로 자바에서는 그 값을 명시적으로 반환해줘야 한다.
(String) -> Unit처럼 반환 타입이 Unit인 함수 타입의 파라미터 위치에 void를 반환하는 자바 람다를 넘길 수는 없다.
함수 타입: 자세한 구현
- 내부에서 코틀린 함수 타입은 일반 인터페이스다.
- 함수 타입의 변수는 FunctionN 인터페이스를 구현한다.
- invoke라는 유일한 메서드가 정의돼있다.
interface Function1<P1, R> {
operator fun invoke(p1: P1): R
}
interface Function2<P1, P2, R> {
operator fun invoke(p1: P1, p2: P2): R
}- 함수 타입의 변수는 함수에 대응하는 FunctionN 인터페이스를 구현하는 클래스 인스턴스에 대한 참조이다.
- invoke 메서드에는 람다 본문이 들어간다.
fun processTheAnswer(f: Function1<Int, Int>) {
println(f.invoke(42))
}- 함수 타입 상속
class Adder: Function2<Int, Int, Int> {
override operator fun invoke(p1: Int, p2: Int): Int{
return p1 + p2
}
}class Adder: (Int, Int) -> Int {
override operator fun invoke(p1: Int, p2: Int): Int{
return p1 + p2
}
}10.1.4 함수 타입의 파라미터에 대해 기본값을 지정할 수 있고, 널이 될 수도 있다.
fun <T> Collection<T>.joinToString( // Collection<T>에 대한 확장 함수 선언
separator: String=", ",
prefix: String="",
postfix: String=""
): String {
val result = StringBuilder(prefix)
for((index, element) in this.withIndex()){ // this 키워드는 수신 객체,
if(index > 0) result.append(separator) // 즉 T 타입의 요소로 이루어진
result.append(element) // 컬렉션을 가리킨다.
}
result.append(postfix)
return result.toString()
}- 이 구현은 핵심 요소인 컬렉션의 각 원소를 문자열로 변환하는 방법을 제어할 수 없다는 단점이 있다.
- 원소를 문자열로 바꾸는 방법을 람다로 전달하면 된다.
- joinToString 함수를 호출할 때마다 매번 람다를 넘기게 만들면 함수 호출을 더 불편하게 만들 수도 있다.
- 함수 타입의 파라미터에 대한 기본값으로 람다식을 대입해 놓음으로써 이런 문제를 해결할 수 있다.
fun <T> Collection<T>.joinToString( // Collection<T>에 대한 확장 함수 선언
separator: String=", ",
prefix: String="",
postfix: String="",
transform: (T) -> String = { it.toString() } // 함수 타입 파라미터를 선언,
): String { // 람다를 기본값으로 지정
val result = StringBuilder(prefix)
for((index, element) in this.withIndex()){ // this 키워드는 수신 객체,
if(index > 0) result.append(separator) // 즉 T 타입의 요소로 이루어진
result.append(transform(element)) // 컬렉션을 가리킨다.
}
result.append(postfix)
return result.toString()
}fun main() {
val letters = listOf("Alpha", "Beta")
println(letters.joinToString())
// Alpha, Beta
println(letters.joinToString{ it.lowercase() })
// alpha, beta
println(letters.joinToString(separator="! ", postfix="! ",
transform= { it.uppercase() }))
// ALPHA! BETA!
}- 널이 될 수 있는 함수 타입을 파라미터로 사용할 수도 있다.
fun foo(callback: (() -> Unit)? ) {
if(callback != null) {
callback()
}
}- 널이 될 수 있는 함수 타입의 변수에 대해서 안전한 호출 구문(?., ?:)으로 invoke 함수를 안전하게 호출할 수 있다. (callback?.invoke())
fun <T> Collection<T>.joinToString( // Collection<T>에 대한 확장 함수 선언
separator: String=", ",
prefix: String="",
postfix: String="",
transform: ((T) -> String)? = null // 함수 타입 파라미터를 선언,
): String { // 람다를 기본값으로 지정
val result = StringBuilder(prefix)
for((index, element) in this.withIndex()){
if(index > 0) result.append(separator)
result.append(transform?.invoke(element) ?: element.toString() )
} // 안전한 호출을 사용해 함수를 호출, 엘비스 연산자를 사용해
// 람다를 지정하지 않은 경우를 처리
result.append(postfix)
return result.toString()
}10.1.5 함수를 함수에서 반환
- 사용자가 선택한 배송 수단에 따라 배송비를 계산하는 방법이 달라질 수 있다.
- 이럴 때 적절한 로직의 함수를 반환하는 함수를 선언해 사용할 수 있다.
enum class Delivery { STANDARD, EXPIRED }
class Order(val itemCount: Int)
fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {
if(delivery == Delivery.EXPIRED){
return { order -> 6 + 2.1 * order.itemCount } // 함수에서 람다 반환
}
return { order -> 1.2 * order.itemCount }
}
fun main() {
val calculator = getShippingCostCalculator(Delivery.EXPIRED)
print("Shipping consts ${calculator(Order(3))}")
// Shipping costs 12.3
}10.1.6 람다를 활용해 중복을 줄여 코드 재사용성 높이기
- 람다를 사용할 수 없는 환경에서는 아주 복잡한 구조를 만들어야만 피할 수 있는 코드 중복도 람다를 활용하면 간결하고 쉽게 제거 가능
- 웹 사이트 방문 기록을 분석하는 예
data class SiteVisit(
val path: String,
val duration: Double,
val os: OS
)
enum class OS {WINDOWS, LINUX, MAC, IOS, ANDROID}
val log = listOf(
SiteVisit("/", 34.0, OS.WINDOWS),
SiteVisit("/", 22.0, OS.MAC),
SiteVisit("/login", 12.0, OS.WINDOWS),
SiteVisit("/signup", 8.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID)
)- 윈도우 사용자의 평균 방문 시간을 출력하기: 사이트 방문 데이터를 하드 코딩
val averageWindowDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()
fun main() {
println(averageWindowDuration)
}- 일반 함수(확장 함수)를 통해 중복 제거: 운영체제 타입 데이터를 파라미터화
fun List<SiteVisit>.averageDurationFor(os: Os) =
filter{ it.os == os }.map(SiteVisit::duration).average()
fun main() {
println(log.averageDurationFor(OS.WINDOWS))
println(log.averageDurationFor(OS.MAC))
}위의 일반 함수로는 다른 상황에서는 코드를 사용할 수 없고 또 다른 함수 선언 필요
모바일 환경 접속자 평균 시간을 구한다면 모바일 환경일 때의 운영체제 타입 데이터는
OS.IOS와OS.ANDROID이므로 현재 선언한 일반 함수로는 대응 불가모바일 환경 접속자 평균 시간을 구하기 위한 또다른 함수 선언 필요
운영체제 타입 데이터를 파라미터화 하는 것이 아니라, 평균을 구할 방문 데이터를 추출(filter)하는 동작 자체를 파라미터화하여 이를 위한 함수 타입 파라미터를 선언하는 고차 함수를 선언하면 코드 중복을 최대한으로 제거할 수 있다.
편리하게 원하는 대로 다양한 기준으로 방문 데이터를 추출할 수 있게 된다.
fun List<Sitevisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
fun main() {
println(
log.averageDurationFor{
it.os in setOf(OS.ANDROID, OS.IOS)
}
)
println(
log.averageDurationFor{
it.os == OS.IOS && it.path == "/signup"
}
)
}코드 중복을 줄일 때 함수 타입이 상당히 도움이 된다.
람다를 사용하면 데이터의 반복을 추출할 수 있을 뿐만 아니라 반복적인 행동도 추출할 수 있다.
10.2 인라인 함수를 사용해 람다의 부가 비용 없애기
코틀린은 보통 람다를 익명 클래스로 컴파일
람다식마다 새로운 클래스가 생기고 람다가 변수를 캡처한 경우 람다 정의가 포함된 코드를 호출하는 시점마다 새로운 객체가 생긴다.
이로 인해 부가 비용이 들고 람다를 사용하는 구현은 똑같은 코드를 직접 실행하는 함수보다 덜 효율적이다.
반복되는 코드를 별도의 라이브러리 함수로 빼내되 직접 실행될 때만큼 효율적인 코드를 컴파일러가 생성하게 만들 수 있다.
inline 변경자를 어떤 함수에 붙이면 컴파일러는 그 함수가 쓰이는 위치에 함수 호출을 생성하는 대신에 함수를 구현하는 코드로 바꿔치기 해준다.
10.2.1 인라이닝이 작동하는 방식
- 어떤 함수를 inline으로 선언하면 그 함수의 본문이 인라인된다.
- 즉, 함수를 호출하는 코드를 함수를 호출하는 바이트 코드 대신에 함수 본문을 번역한 바이트 코드로 컴파일한다.
// 인라인 함수 정의
import java.util.concurrent.locks.Lock
import java.util.concurrent.ReentrantLock
inline fun <T> synchronized(lock: Lock, action:() -> T): T {
lock.lock()
try{
return action()
}finally{
lock.unlock()
}
}
fun main(){
val l = ReentrantLock()
synchronized(l){
// ...
}
}- synchronized 함수를 inline으로 선언했으므로 synchronized를 호출하는 코드는 모두 자바의 synchronized문과 같아진다.
fun foo(l: Lock){
println("Before sync")
synchronized(l){
println("Action")
}
println("After sync")
}- 위 코드는 다음과 같이 컴파일 된다.
fun _foo_(l: Lock) {
println("Before sync")
l.lock()
try{
println("Action")
}finally{
l.unlock()
}
println("After sync")
}중요한 것은 synchronized 함수의 본문 뿐만 아니라 synchronized에 전달된 람다의 본문도 함께 인라이닝된다는 것이다.
람다의 본문에 의해 만들어지는 바이트코드는 그 람다를 사용하는(호출하는) 코드 정의의 일부분으로 간주되기 때문에 코틀린 컴파일러는 그 람다를 함수 인터페이스를 구현하는 익명 클래스로 감싸지 않는다.
인라인 함수를 호출하면서 람다를 넘기는 대신에 함수 타입의 변수를 넘길 수도 있는데
이런 경우엔 인라인 함수를 호출하는 코드 위치에서는 변수에 저장된 람다의 코드를 알 수 없다
따라서 함수 타입의 변수를 넘기는 형태로 인라인 함수를 호출하면 람다 본문은 인라이닝되지 않는다. 대신 synchronized 함수의 본문만 인라이닝된다.
class LockOwmner(val lock: Lock) {
fun runUnderLock(body: () -> Unit){
synchronized(lock, body)
}
}class LockOwmner(val lock: Lock) {
fun runUnderLock(body: () -> Unit){
lock.lock()
try{
body()
}finally{
lock.unlock()
}
}
}- 하나의 인라인 함수를 두 곳에서 각각 다른 람다를 사용해 호출한다면 그 두 호출은 각각 따로 인라이닝된다.
- 인라인 함수의 본문 코드가 호출 지점에 복사되고 각 람다의 본문이 인라인 함수의 본문 코드에서 람다를 사용하는 위치에 복사된다.
10.2.2 인라인 함수의 제약
- 인라이닝을 하는 방식으로 인해, 람다를 사용하는 모든 함수를 인라이닝 할 수 있는 것은 아니다.
- 파라미터로 받은 람다를 다른 변수에 저장하고 나중에 그 변수를 사용한다면 람다를 표현하는 객체가 어딘가는 존재해야 하기 때문에 람다를 인라이닝 할 수 없다.
class FunctionStorage {
val myStoredFunction: ((Int) -> Unit)? = null
inline fun storeFunction(f: (Int) -> Unit) {
myStoredFunction = f
}
}- 일반적으로 인라인 함수의 본문에서 람다식을 바로 호출하거나 다른 인라인 함수의 인자로 전달하는 경우에는 그 람다를 인라이닝 할 수 있다.
- 그런 경우가 아니라면 컴파일러는 "Illegal usage of inline-parameter"라는 메시지와 함께 인라이닝을 금지시킨다.
fun <T,R> Sequence<T>.map(transform: (T) -> R): Sequence<R>{
return TransformingSequence(this, transform)
// transform 인자를 함수 표현, 즉 함수형 인터페이스를 구현하는
// 익명 클래스 인스턴스로 만들 수 밖에 없다
}- 둘 이상의 람다를 인자로 받는 함수에서 일부 람다만 인라이닝하고 싶을 때가 있다.
- 어떤 람다에 너무 많은 코드가 들어가거나 어떤 람다에 인라이닝을 하면 안 되는 코드가 들어갈 가능성이 있는 경우
- 인라이닝하면 안 되는 람다를 파라미터로 받는다면 noinline 변경자를 파라미터 이름 앞에 붙여 인라이닝을 금지할 수 있다.
inline fun foo(inlined: () -> Unit, noinline notInlined: () ->Unit)
{...}- 자바에서도 코틀린에서 정의한 인라인 함수를 호출할 수 있지만 이런 경우 컴파일러는 인라인 함수를 인라이닝하지 않고 일반 함수 호출로 컴파일한다.
10.2.3 컬렉션 연산 인라이닝
- 코틀린 컬렉션에 속하는 List 타입 객체는 filter 함수와 map 함수를 호출할 수 있는데 이 두 함수 모두 인라인 함수이다.
public actual interface List<out E> : Collection<E>public actual interface Collection<out E> : Iterable<E>public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R>filter 함수에 전달된 람다와 map 함수에 전달된 람다 모두 인라이닝된다.
또한 filter와 map을 연쇄해서 사용하는 경우에도 각각 filter 함수와 map 함수에 전달한 람다가 인라이닝된다.
전달한 람다가 모두 인라이닝되기 때문에 성능 상 이점이 있다고 볼 수도 있지만 filter와 map을 연쇄해서 사용하는 코드는 리스트를 걸러낸 결과를 저장하는 중간 리스트를 만들기 때문에 꼭 그렇지만도 않다.
컬렌션 타입 객체에 대해 filter와 map을 연쇄해서 사용하면 중간 리스트를 만들기 때문에 컬렉션의 처리할 원소가 많아지면 중간 리스트를 사용하는 부가 비용도 커지게 된다.
asSequence를 통해 리스트 대신 시퀀스를 사용하면 중간 리스트로 인한 부가 비용을 줄일 수 있다.
하지만 시퀀스에 사용된 람다는 인라이닝되지 않기 때문에 단순히 중간 리스트를 사용하지 않음으로써 성능 향상을 얻을 것이라 기대하여, 즉 지연 계산을 통한 성능 향상만을 기대하여 모든 컬렉션 연산에 asSequence를 붙이려고 해서는 안 된다.
오히려 크기가 작은 컬렉션은 시퀀스 연산보다 일반 컬렉션 연산이 더 성능이 높을 수 있다.
시퀀스를 통해 성능을 향상시킬 수 있는 경우는 컬렉션 크기가 큰 경우뿐이다.
10.2.4 언제 함수를 인라인으로 선언할지 결정
그럼 결국. inline 선언이 가능한 모든 함수들에 대해선 그냥 전부 inline 선언을 하면 좋은 거 아닐까?
그렇지 않다. 이는 좋은 생각이 아니다. inline 키워드를 사용해도 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 높다.
다른 경우엔 애플리케이션의 성능을 프로파일링하고 측정해보면서 따져봐야 한다.
일반 함수 호출의 경우엔 JVM 쪽에서 이미 강력하게 인라이닝을 지원하고 있다.
JVM은 코드 실행을 분석해서 가장 이익이 되는 방향으로 호출을 인라이닝한다.
이런 과정은 바이트 코드를 실제 기계어 코드로 번역하는 과정(JIT)에서 일어난다.
바이트 코드에서는 각 함수 구현이 정확히 한 번만 있으면 되고 그 함수를 호출하는 모든 부분에따로 코드를 중복할 필요가 없다.
하지만 코틀린 인라인 함수는 바이트 코드에서 각 함수 호출 지점을 모두 함수 본문으로 대치하기 때문에 코드 중복이 생긴다.
추가로 함수를 직접 호출하면 스택 트레이스가 더 깔끔해지는 장점이 있다.
람다를 인자로 받는 함수를 인라이닝하면 이익이 더 많다.
인라이닝을 통햇 없앨 수 있는 부가 비용이 상당하다. 함수 호출 비용을 줄일 수 있을 뿐 아니라 람다를 표현하는 클래스와 람다 인스턴스에 해당하는 객체를 만들 필요도 없어진다.
현재의 JVM은 함수 호출과 람다를 인라이닝해줄 정도록 똑똑하지는 못하다.
인라이닝을 사용하면 일반 람다에서는 사용할 수 없는 몇 가지 기능을 사용할 수 있다.
inline 변경자를 함수에 붙일 때는 코드 크기에 주의를 기울여야 한다.
인라이닝하는 함수가 큰 경우 함수의 본문에 해당하는 바이트코드를 모든 호출 지점에 복사해 넣으면 바이트 코드가 전체적으로 너무 커질 수 있다.
10.2.5 withLock, use, useLines로 자원 관리를 위해 인라인된 람다 사용
람다로 중복을 없앨 수 있는 일반적인 패턴 중 한 가지는 어떤 작업을 하기 전에 자원을 획득하고 작업을 마친 후 자원을 해제하는 자원 관리다.
자원: 파일, 락, 데이터베이스 트랜잭션 등
자원 관리 패턴을 만들 때 보통 사용하는 방법은 try/finally 문을 사용하되 try 블록을 시작하기 직전에 자원을 획득하고 finally 블록에서 자원을 해제하는 것이다.
자원 획득과 해제 사이의 try 블록에서는 자원을 사용하는 람다 코드가 위치하고 호출될 것이다.
withLock은 Lock 인터페이스의 확장 함수로 코틀린 라이브러에 속하며 좀 더 코틀린다운 API를 통해 자바의 synchronized 문과 같은 기능을 제공한다.
inline fun <T> Lock.withLock(action:() -> T): T{
lock()
try{
return action()
} finally {
unlock()
}
}val l: Lock = ReentrantLock()
l.withLock{
// 락에 의해 보호되는 자원을 사용, 람다로 표현
}- 이런 패턴을 사용할 수 있는 다른 유형의 자원으로 파일이 있다.
- use 함수는 Closable 인터페이스를 구현 객체에 해당하는 닫을 수 있는 자원에 대해 호출하는 확장 함수다.
- 이 함수는 람다를 호출하고 사용 후 자원이 확실히 닫히게 한다.
fun readFirstLineFromFile(fileName: String): String {
BufferedReader(FileReader(fileName)).use {
br -> return br.readLine()
}
}- use 함수는 inline 함수이다.
- useLines는 File과 Path 객체에 대해 정의된 확장 함수이고 람다가 문자열 시퀀스에 접근하게 해준다.
fun readFirstLineFromFile(fileName: String): String {
Path(fileName).useLines {
return it.first()// 비로컬 return, readFirstLineFromFile 함수를 끝내면서 반환
}
}10.3 람다에서 반환: 고차 함수에서 흐름 제어
- 루프 중간에 있는 return 문의 의미를 이해하기는 쉽다.
- 하지만 filter와 같이 그 루프를 람다를 호출해 구현하는 함수로 바꾼다면 return문의 의미는 뭘까?
10.3.1 람다 안의 return 문: 람다를 둘러싼 함수에서 반환
- 일반 루프 안에서 return 사용
data class Person(val name: Strig, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
fun lookForAlice(people: List<Person>) {
for(person in people) {
if(person.name == "Alice"){
println("Fount!")
return // lookForAlice 함수를 빠져나온다.
}
}
println("Alice is not found")
}
fun main() {
lookForAlice(people)
// Found!!
}- forEach 함수에 전달된 람다 안에서 return
fun lookForAlice(people: List<Person>) {
people.forEach {
if(it.name == "Alice"){
println("Found!")
return// 일반 루프 안에서의 return과 똑같이 lookForAlice 함수를 빠져나온다.
}
}
println("Alice is not found")
}- 람다 안에서 return을 사용하면 람다에서만 반환되는 것이 아니라 그 람다를 호출하는 함수가 실행을 끝내고 반환된다.
- 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return 문을 비로컬 return이라 부른다.
- 자바 메서드 안에 있는 for 루프나 synchronized 블록 안에서 return 키워드가 동작하는 것과 같이 코틀린은 람다를 인자로 받는 함수 안에서 쓰이는 return이 같은 의미를 유지하게 한다.
- 다만 이렇게 return이 바깥쪽 함수를 반환시킬 수 있는 때는 return이 쓰인 람다를 인자로 받는 함수가 인라인 함수인 경우뿐이다.
- 인라이닝되지 않는 함수에 전달되는 람다 안에서 return을 사용할 수 없다.
- 인라이닝되지 않는 함수는 람다를 변수에 저장할 수 있기 때문이다.
- 만약 return을 사용한 람다를 변수에 저장하는 경우, 함수가 반환된 다음에 나중에 람다를 실행하는 상황에서 원래의 함수를 반환시키기에는 너무 늦은 시점일 수 있다.
10.3.2 람다로부터 반환: 레이블을 사용한 return
- 람다식에서도 로컬 return을 사용할 수 있다.
- 람다 안에서 return은 for 루프의 break와 비슷한 역할을 한다.
- 로컬 return은 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 계속 이어간다.
- 람다에서의 로컬 return과 비로컬 return을 구분하기 위해 레이블을 사용한다.
// 레이블을 통해 로컬 return 사용하기
fun lookForAlice(people: List<Person>) {
people.forEach label@{ // 람다식 앞에 레이블을 붙인다.
if(it.name != "Alice") return@label // 로컬 return: 람다로부터 빠져나온다
print("Found Alice!")
}
}- 함수 이름을 return 레이블로 사용하기
fun lookForAlice(people: List<Person>) {
people.forEach { // 이 상황에서 람다식 앞에 레이블을 붙이면 함수 이름 레이블 사용 X
if(it.name != "Alice") return@forEach
print("Found Alice")
}
}10.3.3 익명 함수: 기본적으로 로컬 return
- 비로컬 return은 장황하고, 람다 안에 return 식이 여럿 들어가야 하는 경우 사용하기 불편하다.
- 코틀린은 코드 블록을 전달하기 위한 다른 해법으로 익명 함수를 제공한다.
- 익명 함수는 다른 함수에 전달할 수 있는 코드 블록을 작성하는 다른 형태인데 람다와 익명 함수는 return 식을 쓰는 방식에서 차이가 있다.
fun lookForAlice(people: List<Person>) {
people.forEach(fun (person) {
if(person.name == "Alice") return // 로컬 return: 람다로부터 빠져나온다.
println("${person.name} is not Alice")
})
}
fun main() {
lookForAlice(people)
// Bob is not Alice
}- 익명 함수는 함수 이름을 생략하고 파라미터 타입을 컴파일러가 추론한다.
people.filter(fun (person): Boolean {
return person.age < 30
})people.filter(fun (person) = person.age < 30)- 사실 return에 적용되는 규칙은 단순히 return은 fun 키워드를 사용해 정의된 가장 안쪽 함수를 반환시킨다는 것이다.
- 람다식은 fun을 사용해 정의되지 않으므로 람다 본문의 return은 람다 밖의 함수를 반환시킨 것이다.
'Language > Kotlin' 카테고리의 다른 글
| [OOP] 5. 제네릭스 (2) | 2025.07.21 |
|---|---|
| [OOP] 4. 연산자 오버로딩 (1) | 2025.07.14 |
| [API] 2. 컬렉션과 시퀀스 (1) | 2025.07.07 |
| [OOP] 3. 기본 타입, 컬렉션, 배열 (1) | 2025.06.09 |
| [API] 1. 람다를 사용한 프로그래밍 (3) | 2025.06.04 |