728x90
반응형
람다 식 또는 람다는 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻함
람다를 사용하면 쉽게 공통 코드 구조를 라이브러리 함수로 뽑아낼 수 있음
코틀린 표준 라이브러리는 람다를 아주 많이 사용함
✅ 람다 식과 멤버 참조
📌 람다 소개 : 코드 블록을 함수 인자로 넘기기
- "이벤트가 발생하면 이 핸들러를 실행하자" 등의 수행을 코드로 표현하려면 어떻게 해야할까?
- 자바에서는 무명 내부 클래스를 통해 이런 목적을 달성하였음
- 무명 내부 클래스를 사용하면 코드를 함수에 넘기거나 변수에 저장할 수 있기는 하지만 상당히 번거로울 수 있음
- 하지만 코틀린에서는 함수를 값처럼 다뤄 이러한 문제를 해결함
- 클래스를 선언하고 그 클래스의 인스턴스를 함수에 넘기는 대신 함수형 언어에서는 함수를 직접 다른 함수에 전달
- 람다 식을 사용하면 코드가 더욱 더 간결해지며 함수를 선언할 필요가 없고 코드 블록을 직접 함수의 인자로 전달할 수 있음
// 자바와 코틀린의 setOnclickListener 비교
/* 자바 */
button.setOnClickListener(new OnClicklistener() {
@Override
public void onClick(View view) {
/* 클릭 시 수행할 동작 */
}
});
/* 코틀린 */
button.setOnClickListener { /* 클릭 시 수행할 동작 */ }
📌 람다와 컬렉션
- 코드에서 중복을 제거하는 것은 프로그래밍 스타일을 개선하는 중요한 방법 중 하나
- 람다가 없는 경우 컬렉션을 편리하게 처리할 수 있는 좋은 라이브러리를 제공하기 힘듦
- 하지만 코틀린에서는 람다나 멤버 참조를 인자로 취하는 라이브러리 함수를 사용하여 보다 간결하고 쉽게 코드를 작성할 수 있음
data class Person(val name: String, val age: Int)
/* 람다를 사용하지 않고 컬렉션을 직접 검색하기 */
fun findTheOldes(people: List<Person>) {
var maxAge = 0
var theOldes: Person? = null
for (person in people) {
if(person.age > maxAge) {
maxAge = person.age
theOldest = person
}
}
println(theOldes)
}
val people = listOf(Person("Alice", 29), Person("Bob", 31))
findTheOldes(People)
// Person(name=Bob, age=31)
/* 람다 사용하여 컬렉션 검색하기 */
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy{it.age}) // 나이 프로퍼티를 비교해서 값이 가장 큰 원소 찾기
// Person(name=Bob, age=31)
- 이런 식으로 it.age와 같이 단지 함수나 프로퍼티를 반환하는 역할을 수행하는 람다는 멤버 참조로 대치할 수 있음
- 이러한 방식을 활용하면 코드를 더 짧고 이해하기 쉽게 작성할 수 있음
📌 람다 식의 문법
- 람다 식의 문법은 아래와 같음
- 파라미터 -> 본문 형식을 띄며 항상 중괄호 사이에 위치하도록 해야함
{ 파라미터(x: Int, y: Int) -> 본문(x + y) }
- 또한 람다 식을 변수에 저장할 수 있으며 람다가 저장된 변수를 다른 일반 함수와 마찬가지로 다룰 수 있음
- 변수 이름 뒤에 괄호를 놓고 그 안에 필요한 인자를 넣어서 람다를 호출할 수 있음
val sum = {x: Int, y: Int -> x + y}
println(sum(1, 2)) // 변수에 저장된 람다 호출
// 3
- 또한 원하는 경우 람다 식을 직접 호출할 수 있음
{ println(42) } ()
// 42
- 하지만 위와 같은 구문은 읽기 어렵고 그다지 쓸모가 없기 때문에 run을 활용하여 직접 실행하는 편이 나음
run{println(42)} // 람다 본문에 있는 코드 실행
// 42
- 람다 식에서 함수 호출 시 마지막 인자가 람다식인 경우
- 괄호 밖으로 람다를 뺄 수 있는 문법 관습이 있음
- 또한, 람다가 어떤 함수의 유일한 인자인 경우 빈 괄호를 생략해도 됨
- 컴파일러는 람다 파라미터의 타입을 추론할 수 있기 때문에 타입 명시를 하지 않아도 됨
- 마지막으로 람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있는 경우 it을 바로 사용
// 원래 람다 식
val people = listOf(Person("Alice", 29), Person("Bob", 31))
people.maxBy({p:Person -> p.age})
// 람다 식이 마지막 인자인 경우
people.maxBy() {p:Person -> p.age}
// 람다가 유일한 인자인 경우
people.maxBy {p:Person -> p.age}
// 타입 명시 생략
people.maxBy {p -> p.age}
// 람다의 파라미터가 하나이고, 컴파일러가 타입을 추론할 수 있는 경우
people.maxBy {it.age}
- 람다를 변수에 저장할 때는 파라미터의 타입을 추론할 문맥이 존재하지 않음
- 따라서 파라미터 타입을 명시해야 함
val getAge = {p: Person -> p.age}
people.maxBy(getAge)
- 람다 식은 한 줄로 이뤄진 작은 람다만 있지는 않음
- 본문이 여러 줄로 이뤄진 경우 본문의 맨 마지막에 있는 식이 람다의 return 값이 됨
val sum = {x: Int, y: Int ->
println("Computing the sum of $x and $y ...")
x + y
}
println(sum(1, 2))
// Computing the sum of 1 and 2 ...
// 3
📌 현재 영역에 있는 변수에 접근
- 자바 메서드 안에서 무명 내부 클래스를 정의할 때 메서드의 로컬 변수를 무명 내부 클래스에서 사용할 수 있음
- 람다 안에서도 같은 일을 할 수 있음
- 람다를 함수 안에서 정의하면 함수의 파라미터뿐 아니라 람다 정의의 앞에 선언된 로컬 변수까지 람다에서 모두 사용할 수 있음
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
messages.forEach { // 각 원소에 대해 수행할 작업을 람다로 받음
println("$prefix $it") // 람다 안에서 함수의 "preifx" 파라미터를 사용
}
}
- 자바와 다른 점 중 중요한 한 가지는 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다는 점
- 또한 람다 안에서 바깥의 변수를 변경해도 됨
- 람다 안에서 사용하는 외부 변수를 람다가 포획한 변수라고 부름
fun printProblemCounts(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")
}
❗ 변경 가능한 변수 포획하기 : 자세한 구현 방법
- 파이널 변수를 포획한 경우에는 람다 코드를 변수 값과 함께 저장
- 파이널이 아닌 변수를 포획한 경우에는 변수를 특별한 래퍼로 감싸서 나중에 변경하거나 읽을 수 있게 한 다음, 래퍼에 대한 참조를 람다 코드와 함께 저장
- 한 가지 알아둬야 할 점은 람다를 이벤트 핸들러나 다른 비동기적으로 실행되는 코드로 활용하는 경우 함수 호출이 끝난 다음에 로컬 변수가 변경될 수도 있다는 점
fun tryToCountButtonClicks(button: Button) : Int {
var clicks = 0
button.onClick { clicks++ }
return clicks
}
- onClick 핸들러는 호출될 때마다 clicks의 값을 증가시키지만 그 값의 변경을 관찰할 수는없음
- 핸들러는 tryToCountButtonClicks가 clicks를 반환한 다음에 호출되기 때문
- 이를 해결하기 위해서는 클릭 횟수를 세는 카운터 변수를 함수의 내부가 아닌 클래스의 프로퍼티나 전역 프로퍼티 등의 위치로 빼내 나중에 변수 변화를 살펴볼 수 있게 해야 함
📌 멤버 참조
- 코틀린에서는 자바 8과 마찬가지로 함수를 값으로 바꿀 수 있음
- 이때, 이중 콜론(::)을 활용
val getAge = Person::age
- :: 를 사용하는 식을 멤버 참조라고 함
- 멤버 참조는 프로퍼티나 메서드를 단 하나만 호출하는 함수 값을 만들어 줌
- :: 는 클래스 이름과 참조하려는 멤버(프로퍼티나 메서드) 이름 사이에 위치함
- Person::age 는 같은 역할을 하는 아래 람다 식을 더 간략하게 표현한 것
val getAge = {person: Person -> person.age}
- 참조 대상이 함수인지 프로퍼티인지와는 관계없이 멤버 참조 뒤에는 괄호를 넣으면 안됨
- 멤버 참조는 그 멤버를 호출하는 람다와 같은 타입
people.maxBy{Person::age} // 멤버 참조 표기
people.maxBy{p -> p.age} // 람다 표기
people.maxBy{it.age} // 람다 표기
- 또한, 최상위에 선언된(그리고 다른 클래스의 멤버가 아닌) 함수나 프로퍼티를 참조할 수 있음
fun salute() = println("Salute!")
run(::salute) // 최상위 함수를 참조
// Salute!
- 람다가 인자가 여럿인 다른 함수한테 작업을 위임하는 경우 람다를 정의하지 않고 직접 위임 함수에 대한 참조를 제공하면 편리함
val action = {person: Person, message: String -> // 이 람다는 sendEmail 함수에게 작업을 위임
sendEmail(person, message)
}
val nextAction = ::sendEmail // 람다 대신 멤버 참조를 쓸 수 있음
- 생성자 참조를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있음
- :: 뒤에 클래스 이름을 넣으면 생성자 참조를 만들 수 있음
val createPerson = ::Person // "Person"의 인스턴스를 만드는 동작을 값으로 저장
val p =createPerson("Alice", 29)
println(p)
// Person(name=Alice, age=29)
- 확장 함수도 멤버 함수와 같은 방식으로 참조할 수 있음
fun Person.isAdult() = age >= 21 // 확장 함수 정의
val predicate = Person::isAdult // 확장 함수 참조
728x90
반응형
'Studying > Kotlin' 카테고리의 다른 글
[Kotlin In Action] 5장. 람다로 프로그래밍(3) - 지연 계산(lazy) 컬렉션 연산 (0) | 2023.05.22 |
---|---|
[Kotlin In Action] 5장. 람다로 프로그래밍(2) - 컬렉션 함수형 API (0) | 2023.05.18 |
[Kotlin In Action] 4장. 클래스, 객체, 인터페이스(4) - object 키워드 : 클래스 선언과 인스턴스 생성 (0) | 2023.05.16 |
[Kotlin In Action] 4장. 클래스, 객체, 인터페이스(3) - 컴파일러가 생성한 메서드 : 데이터 클래스와 클래스 위임 (0) | 2023.05.16 |
[Kotlin] 코틀린의 Scope Function(범위 지정 함수) (0) | 2023.05.16 |