람다 안의 return문 : 람다를 둘러싼 함수로부터 반환

이 절에서는 컬렉션에 대한 이터레이션을 두 가지 살펴본다. 다음 코드의 실행 결과를 보면 이름이 Alice인 경우에 lookForAlice 함수로부터 반환된다는 사실을 분명히 알 수 있다.

data class Person(val name: String, 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("Found!")
            return
        }
    }
    println("Alice is not found")
}

fun main() {
    lookForAlice(people)
}

Found!

이 코드를 forEach로 바꿔 써도 될까? forEach 안에 넘긴 람다 안에 있는 return도 앞 예제와 같은 의미다. 따라서 forEach 함수를 대신 써도 안전하다.

fun lookForAlice(people: List<Person>) {
    people.forEach { 
        if (it.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

람다 안에서 return을 사용하면 람다로부터만 반환되는 게 아니라 그 람다를 호출하는 함수가 실행을 끝내고 반환된다. 그렇게 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return 문을 non-local return이라 부른다.

이 규칙 뒤에 숨어있는 로직을 이해하려면 자바 메소드 안에 있는 for 루프나 synchronized 블록 안에서 return 키워드가 어떻게 동작하는지 살펴보면 된다. 그런 경우 return은 for 루프나 synchronized 블록을 끝내지 않고 메소드를 반환시킨다. 코틀린에서는 언어가 제공하는 기본 구성 요소가 아니라 람다를 받는 함수로 for나 synchronized와 같은 기능을 구현한다. 코틀린은 그런 함수 안에서 쓰이는 return이 자바의 return과 같은 의미를 갖게 허용한다.

이렇게 return이 바깥쪽 함수를 반환시킬 수 있는 때는 람다를 인자로 받는 함수가 인라인 함수인 경우뿐이다. forEach는 인라인 함수이므로 람다 본문과 함께 인라이닝된다. 따라서 return 식이 바깥쪽 함수를 반환시키도록 쉽게 컴파일할 수 있다. 하지만 인라이닝되지 않는 함수에 전달되는 람다안에서 return을 사용할 수는 없다. 인라이닝되지 않는 함수는 람다를 변수에 저장할 수 있고, 바깥쪽 함수로부터 반환된 뒤에 저장해 둔 람다가 호출될 수도 있다. 그런 경우 람다 안의 return이 실행되는 시점이 바깥쪽 함수를 반환시키기엔 너무 늦은 시점일 수도 있다.

람다로부터 반환: 레이블을 사용한 return

람다 식에서도 local return을 사용할 수 있다. 람다 안에서 로컬 return은 for 루프의 break와 비슷한 역할을 한다. 로컬 return은 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 계속 이어간다. 로컬 return과 non-local return을 구분하기 위해 label을 사용해야 한다. return으로 실행을 끝내고 싶은 람다 식 앞에 레이블을 붙이고, return 키워드 뒤에 그 레이블을 추가하면 된다.

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach label@{ // 람다 식 앞에 레이블을 붙인다.
        if (it.name == "Alice") {
            return@label // return@label은 앞에서 정의한 레이블을 참조한다.
        }
    }
    println("Alice might be someWhere") // 항상 이 줄이 출력된다.
}

fun main() {
    lookForAlice(people)
}

Alice might be someWhere

람다 식에 레이블을 붙이려면 레이블 이름 뒤에 @ 문자를 추가한 것을 람다를 여는 { 앞에 넣으면 된다. 람다로부터 반환하려면 return 키워드 뒤에 @ 문자와 레이블을 차례로 추가하면 된다.

제목 없음.png

람다에 레이블을 붙여서 사용하는 대신 람다를 인자로 받는 인라인 함수의 이름을 return 뒤에 레이블로 사용해도 된다.

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            return@forEach
        }
    }
    println("Alice might be someWhere")
}

람다 식의 레이블을 명시하면 함수 이름을 레이블로 사용할 수 없다.

람다 식에는 레이블이 2개 이상 붙을 수 없다.

레이블이 붙은 this 식

this 식의 레이블에도 마찬가지 규칙이 적용된다. 5장에서 수신 객체 지정 람다에 대해 설명했다. 수신 객체 지정 람다의 본문에서는 this 참조를 사용해 묵시적인 컨텍스트 객체(람다를 만들 때 지정한 수신 객체)를 가리킬 수 있다. 수신 객체 지정 람다 앞에 레이블을 붙인 경우 this 뒤에 그 레이블을 붙여서 묵시적인 컨텍스트 객체를 지정할 수 있다.