요즘 Kotlin in Action 을 보면서 예전에 람다에 대해서 정리한 글과 합쳐서 다시 쓰려고한다.
람다란 무엇인가?
람다는 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.
즉, 값처럼 여기저기 전달할 수 있는 동작의 모음이라는 뜻이다.
실행시점에 코틀린 람다 호출에는 아무 부가 비용이 들지 않고, 프로그램의 기본 구성요소와 비슷한 성능을 낸다.
람다는 왜 쓰는가?
1. 코드를 깔끔하게 사용하기 위해
람다가 메서드가 하나뿐인 무명 객체 대신 사용할 수 있다는 사실을 떠올려본다.
안드로이드 개발하며 모두 뷰에 setOnClickListener 를 달아본적이 있을 것이다.
setOnClickListener 에onClick 이라는 메서드 가 있는 OnClickListener 를 구현하여 인자로 전달한다.
람다가 나오기 이전의 자바에서는 이를 무명 내부 클래스를 사용하여 코드를 작성하였다.
하지만 이는 매우 번거롭다.
// 무명 내부 클래스 선언하는 방식 (자바)
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) { }
});
// 람다 사용
button.setOnClickListener { }
2. 일련의 동작을 변수에 저장하거나 다른 함수에 넘겨야하는 경우에 사용하기 위해
일련의 동작을 변수에 저장하거나 다른 함수에 넘겨야하는 경우는 콜렉션을 다루는 코틀린 표준 라이브러리를 생각하면 떠올리기 쉽다.
예를 들어 filter 같은 경우, 참/거짓을 반환하는 predicate 를 인자로 전달해주면, 그 predicate 가 true 로 반환하는 원소만 가지는 컬렉션을 반환해준다. 여기서 predicate 로 전달하는 것이 람다이다.
각 원소를 조회하며 predicate 로 전달된 람다를 실행하는 것이다.
val list = listOf(1, 2, 3, 4, 5)
println(list.filter { it % 2 == 0 })
// 2, 4 만 출력횐다.
이런 경우 주의할 점은 인자로 전달된 람다가 함수 내부에서 어떻게 사용되는지에 따라 시간복잡도를 고려해야한다는 점이다.
filter 가 모든 원소를 확인하므로 기본 O(N) 으로 시작하고 predicate 로 전달한 람다에서도 O(N) 의 시간복잡도를 가진다면 총 시간복잡도는 O(N^2) 이 될 것이다.
람다 문법
{ 파라미터: 타입, 파라미터: 타입 ... -> 본문 }
예를들어 나이와 이름을 인자로받아 하나의 문자열로 합쳐서 반환하는 람다가 있다고 가정하고 변수를 초기화하는 코드는 아래와 같다.
val info: (Int, String) -> String = { age: Int, name: String -> "$age $name" }
여기서 몇가지 규칙이 있다.
- 예시에 나와있듯 람다의 마지막 문장이 반환이다.
- 쓰지않는 파라미터는 _ 로 생략하여 사용할 수 있다.
- 예를 들어 나이를 두배로 해서 String 으로 반환하는 요구사항으로 바꾼다고 했을 때 아래와 같이 표현할 수 있다
- { age: Int, _ -> "${age * 2}" }
- 로컬 변수처럼 컴파일러는 람다 파라미터의 타입도 추론할 수 있다. 따라서 파라미터 타입을 명시할 필요 없다.
- 위 예시에서 { age, name -> "$age $name" } 로 적어도 된다는 뜻이다.
- 인자가 단 하나뿐이고 컴파일러가 타입을 추론할 수 있는 경우 굳이 인자에 이름을 붙이지 않고 it 으로 명하여 사용할 수 있다.
- 예를 들어 인자가 나이 하나였고, 나이를 두배로 해서 String 으로 반환하는 요구사항으로 바꾼다고 했을 때, 아래와 같이 표현할 수 있다 다 생략하고 it 으로 대체해서 아래와 같이 사용할 수 있다.
- { "${it * 2}" }
- 중첩된 경우 남용하지 않게 주의한다. it 이 의미하는 객체가 두가지일 수 있기 때문이다. 따라서 중첩인 경우에는 인자를 생략하지 않고 인자에 이름을 붙이는 게 좋겠다.
- 함수 호출 시 맨 뒤에 있는 인자가 람다 식이라면 그 람다를 괄호 밖으로 빼낼 수 있다.
- 람다가 유일한 인자인 경우도 해당하고, 이 경우 괄호도 생략 가능하다.
- filter 도 이에 해당한다. 그래서 list.filter { it % 2 == 0 } 처럼 사용할 수 있었던 것이다.
- 반환이 없는 람다의 타입은 일반 함수처럼 Unit 을적어주면 된다. (아래 Fish 예제 myWith 참고)
멤버 참조로 람다만들기
멤버참조는 그 멤버를 호출하는 람다와 같은 타입이다.
넘기려는 코드가 이미 함수로 선언된 경우는 이중콜론(::) 을 사용해서 람다로 만들 수 있다.
:: 을 사용하는 식을 멤버참조라고 부르는데 유용한 경우가 있다.
예를 들어 구매처리를 하는 함수를 다른 코드에서도 호출하는데, 람다로도 사용하고 싶다면?
아래 코드와 같이 alsoWantToUseLambda 라는 함수를 인자로도 전달하고 싶으면 A::alsoWantToUseLambda 로 전달하면 된다.
같은 방식으로 프로퍼티도 참조할 수 있다.
class A {
fun alsoWantToUseLambda(a: Int): String {
// ...
}
}
---
fun functionName(lambda: (Int) -> String) {
// ...
}
---
functionName(A::alsoWantToUseLambda)
또한 멤버참조는 프로퍼티도 참조할 수 있다. 의아할 수도 있는데,
멤버참조가 그 멤버를 호출하는 람다와 같은 타입이라는 말을 떠올리면 편하다.
maxBy 는 리스트를 돌면서 가장 큰 원소를 반환하는데, 무엇에 대해서 최대인지 람다로 전달할 수 있다.
멤버참조를 사용하여 아래와 같은 코드를 작성할 수 있다는 말이다.
data class Person(
val name: String,
val age: Int
)
val list = listOf(
Person(
"1",
1
),
Person(
"3",
3
),
Person(
"2",
2
)
)
list.maxBy(Person::age)
list.maxBy { it.age }
출처: Kotlin in Action
'Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린의 연산자 관례 정리, 어떤 클래스든 연산시켜보자! (0) | 2023.01.08 |
---|---|
[Kotlin] 흥미돋는 코틀린의 타입을 정리해보자 (0) | 2023.01.05 |
[Kotlin] Pair , Triple 과 Destructure (0) | 2021.08.05 |
[Kotlin] 상속/ interface/ abstract class/ object/ data class/ enum class (0) | 2021.08.05 |
[Kotlin] 접근제어자 ( visibility modifiers ) (0) | 2021.08.04 |