Post

(Kotlin) 람다(lambda), 변수 포획과 클로저, 멤버 참조

lambda

1
{ x: Int, y: Int -> x + y }
  • 코틀린 람다는 자바 람다와 달리 항상 중괄호 사이에 위치한다.
  • 반면 메서드 참조는 중괄호 없이 써야 한다.

코틀린 람다 호출은 실행 시점에 아무런 부가 비용이 들지 않으며 기본 구성 요소와 비슷한 성능을 내기 때문에 적절하게 사용하는 것이 권장된다.

코틀린은 파이썬처럼 컬렉션의 최댓값을 찾거나 하는 기능이 기본적으로 구현되어 있기 때문에, 일단 한번 찾아보고 없으면 구현한다. 이런 컬렉션 라이브러리들이 보통 함수형으로 되어 있기 때문에 적용할 함수를 넘겨야 하는데, 이런 경우 람다를 사용하는 것이 좋다.

1
2
3
>>> val people = listOf(Person("A", 23), Person("B", 25))
>>> println(people.maxBy({ p -> p.age }) )
People(name=B, age=25)

* 람다 파라미터의 타입도 자동으로 추론해주기 때문에 굳이 명시할 필요가 없다. 못찾는 경우, 필요한 경우에만 적어준다. * 본문이 여러 줄로 이루어진 경우 expression body처럼 마지막 줄이 리턴된다. * 객체 식(무명 객체) 처럼 바깥쪽 변수의 접근/수정이 가능하다. 가독성을 위해 몇 가지를 개선할 수 있는 문법을 지원한다. 모두 동일한 구문이고, 가독성을 위해 여러가지 형태를 지원하는 것 뿐이므로 보기 좋은 형태로 적으면 된다. 단, 일관성 있게.

1. 함수 호출 시 맨 마지막 인자가 람다식이라면 이를 괄호 밖으로 빼낼 수 있다.
1
people.maxBy() { p -> p. age }

무조건 사용한다고 가독성이 올라가는건 아닌 듯. with문 같은 경우 이 기능을 활용해야 언어에서 제공하는 문법 처럼 사용할 수 있기 때문에 여기에 많이 사용하고, 리스너 연결할 때 이벤트 발생 시 실행되는 코드 조각이라는 느낌으로 사용할 때도 많이 사용한다.

2. 람다가 어떤 함수의 유일한 인자인 경우, 함수 호출 괄호를 없애도 된다.
1
people.maxBy { p -> p. age }
3. 람다의 파라미터가 하나뿐이고, 그 타입을 컴파일러가 추론할 수 있는 경우 it을 바로 쓸 수 있다.
1
people.maxBy { it.age }

간단하고 직관적이라서 자주 쓰게 된다. 가독성에도 도움이 되는 듯. 그러나 람다 안에 람다가 중첩되는 경우에는 각각의 it이 가리키는 파라미터가 어떤 람다에 속해 있는지 파악하기 어려울 수 있으므로, 이런 경우에는 사용하지 않는 것이 좋다.

변수 포획

람다 안에서 사용하는 외부 변수를 람다가 포획(capture)한 변수라고 부른다. 실행 시점에 람다를 표현하기 위해서는람다에서 시작하는 모든 참조 (reference임. 값이 아님.)가 포함된 닫힌(closed) 객체 그래프를 람다 코드와 함께 저장 **해야 한다.이런 데이터 구조(닫힌 객체 그래프+람다 코드)를 클로저(closure)**라고 부른다. 기본적으로 함수 안에 정의된 로컬 변수의 life cycle은 함수의 life cycle과 같다. 즉, 함수가 종료되면서 끝난다. 그러나 어떤 함수가 자신의 로컬 변수를 포획한 람다를 반환하거나, 다른 변수에 저장한다면 함수가 종료되어도 로컬 변수가 사라지지 않아 람다의 본문 코드가 포획한 변수에 접근/수정할 수 있다.(closure)

  • final변수를 포획한 경우, 변수 값을 람다 코드와 함께 저장한다.
  • final이 아닌 변수를 포획한 경우, 변수를 특별한 래퍼와 감싸서 나중에 변경하거나 읽을 수 있도록 한 다음, 래퍼에 대한 참조를 람다 코드와 함께 저장한다.
  • 이는 자바에서는 원래 final만 포획할 수 있기 때문이다. 자바에서 final이 아닌 변수를 포획하기 위해서는 이 변수를 담을 수 있는 배열이나 클래스를 만들고, 그 배열이나 클래스 자체는 final로 만드는 방법을 사용해야 한다. 코틀린에서 다음과 같은 코드는
1
2
var counter = 0
val inc = { counter++ }

자바에서 이렇게 변환된다.

1
2
3
4
5
6
7
8
9
10
11
12
final IntRef counter = new IntRef();
counter.element = 0;
Function0 inc = (Function0)(new Function0() {
    public Object invoke() {
        return this.invoke();
    }
 
    public final int invoke() {
        int var1 = counter.element++;
        return var1;
    }
});
클로저의 잘못된 사용 예
1
2
3
4
5
fun tryToCountButtonClicks(button: Button): Int {
    var clicks = 0    // 호출될 때 마다 clicks는 0으로 초기화!!!!
    button.onClick { clicks++ }    // 람다를 호출하는게 아니라, 람다를 핸들러에 등록.
    return clicks    // clicks를 리턴하기는 하는데, 핸들러가 호출되지 않았으면 항상 0이 호출됨.
}

함수의 호출 결과는 항상 0이다. 다른 값이 리턴되려면 clicks를 초기화하고 핸들러를 등록하고, 리턴하는 사이에 핸들러가 호출되어야 한다. 이런 식으로 count를 측정하고 싶은 경우 clicks를 프로퍼티로 빼내야 한다.

멤버 참조 (member reference)

멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수(값) 를 만들어준다.

1
2
Person::age    // is correspond to
val getAge = { person: Person -> person.age }

java.lang.Class를 넘겨야 하는 경우 ::class.java 를 이용해 클래스 참조를 얻어낼 수 있다. 주로 안드로이드에서 인텐트를 생성할 때 사용한다.

1
2
Intent downloadIntent = new Intent(this, DownloadService.class);    // java
val downloadIntent = Intent(this, DownloadService::class.java)    // kotlin

멤버 참조는 그 멤버를 호출하는 람다와 자유롭게 바꿔 쓸 수 있다. 이렇게 함수 f{ x -> f(x) }를 서로 바꿔 쓰는 것을 함수형에서는 에타 변환이라고 부른다.

1
2
people.maxBy({ p -> p. age })
people.maxBy(Person::age)
  • 최상위에 선언된 함수나 프로퍼티를 참조하려면 앞을 비워두고 ::만 적는다.
  • 생성자도 참조할 수 있다. 생성자 참조는 ::ClassName으로 적는다.
  • 확장 함수도 참조할 수 있다.

바운드 멤버 참조 를 사용하면 멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장하기 때문에 객체를 넘길 필요가 없어진다.

1
2
3
4
5
6
7
>>> val p = Person("umbum", 25)
>>> val personAgeFunction = Person::age
>>> println(personAgeFunction(p))
25
>>> val umbumAgeFunction = p::age    // bound member reference
>>> println(umbumAgeFunction())
25

Kotlin lambda를 Java8의 FunctionalInterface로 변환하는 방법

Kotlin에서는 Lambda를 아예 타입으로 지원하기 때문에 그냥 쓰면 FunctionalInterface와 호환이 되지 않는다.

아래와 같이 직접 생성하는 방식 사용해야 함.

1
2
3
Consumer<HttpHeaders> { headers ->
    headers.add("..", "...")
}
This post is licensed under CC BY 4.0 by the author.