[코틀린] 람다 - 람다 식과 멤버 참조

hee09·2021년 11월 30일
0
post-thumbnail

람다 식과 멤버 참조

람다 식 또는 람다는 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻합니다. 코틀린 표준 라이브러리는 람다를 아주 많이 사용합니다. 람다를 자주 사용하는 경우로는 컬렉션 처리가 있습니다.

람다 식의 문법

람다는 앞서 말했듯이 값처럼 여기저기 전달할 수 있는 동작의 모음입니다. 람다를 따로 선언해서 변수에 저장할 수도 있습니다. 하지만 함수에 인자로 넘기면서 바로 람다를 정의하는 경우가 대부분입니다. 아래는 람다 식을 선언하기 위한 문법을 보여줍니다.

코틀린 람다 식은 항상 중괄호로 둘러싸여 있습니다. 인자 목록 주변에 괄호가 없으며, 화살표(->)가 인자 목록과 람다 본문을 구분해줍니다.

람다 식을 변수에 저장할 수 있습니다. 람다가 저장된 변수를 다른 일반 함수와 마찬가지로 다루면 됩니다(변수 이름 뒤에 괄호를 놓고 그 안에 필요한 인자를 넣어서 람다를 호출)

변수에 람다식 저장

// 람다 식을 변수에 저장
val sum = {x: Int, y: Int -> x + y}
// 일반 함수를 호출하는 것과 같이 호출
println(sum(1,2))

실행 시점에 코틀린 람다 호출에는 아무 부가 비용이 들지 않고 프로그램의 기본 구성 요소와 비슷한 성능을 냅니다. 이제 코틀린 코드에서 쓰이는 람다식을 개선해나가며 람다의 이점을 알아보겠습니다.

람다 식

maxByOrNull 메서드에 인자로 람다를 전달

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

fun main() {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    
    people.maxByOrNull ({ person: Person -> person.age })
}

프로퍼티를 두 개를 가지고있는 data 클래스 선언하고 리스트를 생성하였습니다. 그리고 리스트 객체의 maxByOrNull() 함수를 호출하며 인자로 람다를 전달하고 있습니다. maxByOrNull()은 컬렉션을 다루는 코틀린 라이브러리 함수로 기능은 컬렉션을 순환하며 컬렉션이 null이라면 null을 반환하고 nul이 아니라면 인자로 들어온 요소 중 가장 큰 값을 반환하는 함수입니다. 컬렉션과 관련된 라이브러리 함수는 컬렉션 함수형 API에서 추가로 다루겠습니다. 위에서 전달한 람다는 가장 기본적인 형태의 람다인데 Person 타입의 값을 인자로 받아서 인자의 age를 반환하고 있습니다.

개선 1. 괄호 밖으로

people.maxByOrNull (){ person: Person -> person.age }

코틀린에서는 함수 호출 시 맨 뒤에 있는 인자가 람다 식이라면 그 람다를 괄호 밖으로 빼낼 수 있다는 문법 관습이 있습니다. 그렇기에 위 예제에서는 람다가 유일한 인자이자 마지막 인자이므로 괄호 뒤에 람다를 두었습니다.

개선 2. 괄호 제거

people.maxByOrNull { person: Person -> person.age }

람다가 어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 사용했다면 호출 시 빈 괄호를 없애도 됩니다. 다만 주의할 점은 둘 이상의 람다를 인자로 받는 함수라고 해도 인자 목록의 맨 마지막 람다만 괄호 밖으로 뺄 수 있습니다.

개선 3. 파라미터 타입 제거

people.maxByOrNull { person -> person.age }

로컬 변수처럼 컴파일러는 람다 파라미터의 타입을 추론할 수도 있습니다. 따라서 파라미터 타입을 명시할 필요가 없습니다. 다만 컴파일러가 람다 파라미터의 타입을 추론하지 못하는 경우도 있기 때문에 처음에는 타입을 쓰지 않고 람다를 작성하고 컴파일러에서 에러가 발생하는 경우에만 타입을 명시하라고 나와있습니다.

개선 4. it을 사용

people.maxByOrNull { it.age }

람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있는 경우 it을 바로 쓸 수 있습니다. 람다 파라미터 이름을 따로 지정하지 않은 경우에만 it이라는 이름이 자동으로 만들어집니다.

주의

  • 람다를 변수에 저장할 때는 파라미터 타입을 추론할 문맥이 존재하지 않습니다. 따라서 반드시 파라미터 타입을 명시해야 합니다.

  • 본문이 여러 줄로 이뤄진 경우 본문의 맨 마지막에 있는 식이 람다의 결과 값이 됩니다.


현재 영역에 있는 변수에 접근

자바 메서드 안에서 무명 내부 클래스를 정의할 때 메소드의 로컬 변수를 무명 내부 클래스에서 사용할 수 있습니다. 코틀린의 람다도 같은 일을 할 수 있는데 람다를 함수 안에서 정의하면 함수의 파라미터뿐 아니라 람다 정의의 앞에 선언된 로컬 변수까지 람다에서 모두 사용할 수 있습니다.

함수 파라미터를 람다 안에서 사용

fun printMessageWithPrefix(messages: Collection<String>, prefix: String) {
    messages.forEach {
        println("$prefix $it")
    }
}

람다식 안에서 함수의 파라미터를 사용하는 예제입니다. 자바와 다른 점 중 중요한 한 가지는 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다는 점입니다. 또한 람다 안에서 바깥의 변수를 변경해도 됩니다.

바깥의 변수를 변경

fun printProblemsCounts(responses: Collection<String>) {
    // 람다에서 사용할 변수
    var clientErrors = 0
    var serverErrors = 0

    responses.forEach {
        // 람다 안에서 람다 밖의 변수를 변경
        if(it.startsWith("4")) {
            clientErrors++
        } else if(it.startsWith("5")) {
            serverErrors++
        }
    }

    println("$clientErrors client errors, $serverErrors server errors")
}

위 예제들의 prefix, clientErrors, serverErrors와 같이 람다 안에서 사용하는 외부 변수를 '람다가 포획한 변수'라고 부릅니다.

기본적으로 함수 안에 정의된 로컬 변수의 생명주기는 함수가 반환되면 끝납니다. 하지만 어떤 함수가 자신의 로컬 변수를 포획한 람다를 반환하거나 다른 변수에 저장한다면 로컬 변수의 생명주기와 함수의 생명주기가 달라질 수 있습니다. 포획한 변수가 있는 람다를 저장해서 함수가 끝난 뒤에 실행해도 람다의 본문 코드는 여전히 포획한 변수를 읽거나 쓸 수 있습니다. 그 이유는 파이널 변수를 포획한 경우는 람다 코드를 변수 값과 함께 저장하고, 파이널이 아닌 변수를 포획한 경우는 변수를 특별한 래퍼로 감싸서 나중에 변경하거나 읽을 수 있게 한 다음, 래퍼에 대한 참조를 람다 코드와 함께 저장하기 때문입니다.

람다가 이벤트 핸들러나 다른 비동기적으로 실행되는 코드로 활용되는 경우 함수 호출이 끝난 다음에 로컬 변수가 변경될 수도 있다는 것을 알아둬야 합니다.


멤버 참조

코틀린은 자바 8과 마찬가지로 함수를 값으로 바꿀 수 있습니다. 이때 이중 콜론(::)을 사용합니다.

  • ::을 사용하는 식을 멤버 참조라고 부릅니다
  • 멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어줍니다
  • ::는 클래스의 이름과 참조하려는 멤버(프로퍼티나 메서드) 이름 사이에 위치합니다.
  • 참조 대상이 함수인지 프로퍼티인지와는 관계 없이 멤버 참조 뒤에는 괄호를 넣으면 안됩니다.
  • 멤버 참조는 그 멤버를 호출하는 람다와 같은 타입입니다.

람다와 멤버 참조 비교

// 아래는 모두 같은 코드
people.maxByOrNull(Person::age)
people.maxByOrNull{ p -> p.age }
people.maxByOrNull { it.age }

  • 최상위에 선언된 함수나 프로퍼티도 참조할 수 있습니다.
 // 최상위 함수 선언
fun topLevelFunction() {
    println("topLevelFunction")
}


fun main() {
    // run 함수의 인자로 최상위 함수 참조(최상위 함수는 클래스가 없음)
    run(::topLevelFunction)
}

  • 생성자 참조(construct reference)를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있습니다. :: 뒤에 클래스 이름을 넣으면 생성자 참조를 만들 수 있습니다.
data class Person(val name: String, val age: Int)


fun main() {
    // 생성자 참조
    val createPerson = ::Person
    val p = createPerson("Alice", 29)
}

  • 확장 함수도 멤버 함수와 똑같은 방식으로 참조할 수 있습니다.
// 확장 함수 선언
fun Person.isAdult() = age >= 20
// 확장 함수 참조
val predicate = Person::isAdult

참조
Kotlin in Action

틀린 부분 댓글로 남겨주시면 수정하겠습니다..!!

profile
되새기기 위해 기록

0개의 댓글