[출처 - Kotlin In Action] [아래 내용들은 Kotlin In Action을 공부하면서 스스로 정리한 내용입니다]
람다 식(또는 람다)는 함수를 값처러 다루는 식으로 객체지향보다 함수지향 언어에 가깝습니다. 람다식은 주로 고차 함수에 인자로 전달되거나 고차 함수가 돌려주는 결과값으로 쓰입니다. 그리고 코틀린은 이런 람다식을 많이 사용합니다.
람다 소개
람다식을 사용하면 함수를 선언할 필요가 없고 코드 블록을 직접 함수의 인자로 전달할 수 있습니다.
다음은 안드로이드에서 button에 clickListener를 적용하는 코드 입니다.
button.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View view){
// 클릭 시 수행
}
});
위와 같이 button에 OnClickListenr를 등록을 하면 할 수록 코드는 점점 더 복잡해 질것입니다. 하지만 람다식을 사용하면 다음과 같이 간결해집니다.
button.setOnClickListener{ /* 클릭시 수행 동작 */}
또한 컬렉션에서 람다를 사용하면 더 간결하게 코드를 만들 수 있습니다. 다음 코드는 람다를 사용하지 않은 코드로 가장 나이 많은 사람을 찾는 코드입니다.
data class Person(val name: String, val age: Int)
fun findTheOldest(people: List<Person>) {
var maxAge = 0
var theOldest: Person? = null
for (person in people) {
if (person.age > maxAge) {
maxAge = person.age
theOldest = person
}
}
println(theOldest)
}
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
findTheOldest(people)
}
간단한 연장자를 찾는 코드에도 이렇게 많은 코드가 들어값니다. 하지만 코틀린에서는 위와 같이 개발자가 직접 알고리즘을 만들지 않고 코틀린에서 제공하는 라이브러리 함수로 해결 할 수 있습니다.
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy { it.age })
}
maxBy라는 라이브러리 함수를 통하여 들어가는 인자에 it.age로 Person class의 파라미터인 age로 비교를 하여 제일 큰 값을 반환합니다. 반환시 people 이 List<Person> 타입이므로 Person 형태로 반환하게 됩니다. 그리고 Person은 data 클래스므로 Person(name=Bob, age=31) 이렇게 결가과 나타납니다. 그리고 이런 부분은 다음과 같이 멤버 참조로 대체 할 수 있습니다.
println(people.maxBy(Person::age))
이렇게 람다나 멤버참조를 하는 라이브러리를 사용하면 더 간단하게 그리고 직관적으로 코드를 작성할 수 있습니다.
람다 식
람다식을 선언하는 문법은 다음과 같습니다.
{ x: Int, y: Int -> x + y }
람다식은 항상 중괄호 사이에 있어야 하며 x: Int, y: Int 은 파라미터 -> 뒤의 x + y은 본문이 됩니다. 즉 -> 화살표가 목록과 본문을 구분해줍니다.
람다식은 변수에 저장을 할 수 있습니다.
val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2))
아니면 직접 람다식을 호출해서 사용할 수 있습니다.
{ println(42) }()
하지만 위와 같은 람다식은 이해도 안되고 쓸대가 없습니다. 이런게 사용하지말고 람다식을 인자로 받아서 실행해주는 run을 사용하여 좋습니다. (run에 대해서는 여기서 확인하면 좋습니다.)
run { println(42) }
실행 시점에서 코틀린 람다는 호출에 아무 부가 비용이 들지 않으며, 프로그램의 기본 구성 요소와 비슷한 성능을 냅니다.
그럼 위에서 maxby를 썻던 구문을 확실하게 람다식으로 수정해보겠습니다.
people.maxBy({p:Person -> p.age})
위 코드를 분석하면 maxBy에 람다식을 넘기는데 람다식의 파라미터는 p 즉 Person이 되고, 파라미터인 p.age를 반환하게 됩니다. 그리고 함수 식에서 맨뒤에 있는 인자가 람다식이라면
people.maxBy(){p:Person -> p.age}
이렇게 괄호 밖으로 뺄 수 있습니다.
people.maxBy{p:Person -> p.age}
그리고 위와 같인 중괄호만 쓸 수 있는 이유는 람다 식이 함수의 유일한 인자이기 때문에 괄호 없이 람다식의 중괄호 쓸 수 있습니다.
위 3가지 방식중 마지막이 가장 간결해 사용하기 좋겠지만, 인자가 여러개거나 람다식이 여러개 일 경우에는 다른 방식도 사용 해야합니다. 다음 코드를 보면은 두번째 경우에 대해 다시 확인 해 볼 수 있습니다.
data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
val names = people.joinToString(separator = " ",
transform = { p: Person -> p.name })
println(names)
}
joinToString 함수를 사용하고 있습니다. 이 함수의 원본을 보면
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}
구분자, prefix, posfix, limit, truncated, transform 이렇게 인자를 받고 있고, transform은 람다식으로 받고 있습니다.
(truncated은 잘린 문자열 표현 방식입니다.) 그리고 이 함수의 마지막이 람다 식이므로 다음과 같이 바꿀 수도 있습니다.
val names = people.joinToString(separator = " "){ p: Person -> p.name }
위의 코드 joinToString을 이해하고 있으면 더 간결하고 직관적일 수 있지만, joinToString을 이해하지 못하는 사람이 볼경우 어떻게 동작하는지 이해하기 더 힘들 것입니다.
그럼 다시 위의 maxby를 더 간결하게 만들어 보겠습니다.
people.maxBy{p:Person -> p.age}
위에서는 이렇게 사용했지만 사실은
people.maxBy{p -> p.age}
이렇게 p의 타입을 지정하지 않아도 됩니다. 그 이유는 컴파일러가 호출 타입을 추론하여 적용하기 때문입니다. ( 100% 타입을 추론하는건 아닙니다.
val age = {p:Person->p.age}
people.maxBy(age)
위와 같이 변수에 람다식을 대입하면 컴파일러가 타입을 추론하지 못하기 때문에 지정해 주어야 합니다. 컴파일러가 추론못한다고 error를 뱉으면 타입을 명시하면 됩니다.)
그리고 마지막으로 람다의 파라미터 이름을 디폴트 이름인 it으로 바꾸면 더 간단하게 람다식을 작성할 수 있습니다.
people.maxBy { it.age }
it을 사용할 수 있는 이유는 파라미터가 1개 뿐이고 컴파일러가 그 타입을 추론할 수 있기 때문입니다. (코틀린의 컴파일러는 정말 똑똑합니다.) 그리고 람다 파라미터의 이름을 따로 지정하지 않으면 it이라는 이름 만들어 집니다.
람다식은 1줄만 쓸 수 있는게 아니고 여러줄로 이뤄진 경우도 작성할 수있습니다. 그리고 그 람다식의 마지막 줄이 람다의 결과가 됩니다.
변수 접근
람다는 함수의 파라미터 뿐만 아니라 람다 정의 밖에 선언된 로컬 변수까지 람다에서 사용할 수 있습니다.
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
messages.forEach {str->println("$prefix $str")}
}
fun main(args: Array<String>) {
val errors = listOf("403 Forbidden", "404 Not Found")
printMessagesWithPrefix(errors, "Error:")
}
forEach에 {str->println("$prefix $str")} 람다문을 추가하고 그 내용을 보면 함수의 파라미터인 prefix를 사용하고 있는것을 확인 할 수 있습니다. (그리고 이 람다에서 str은 messages에 포함된 String 데이터이 됩니다. 그래서 위 코드의 결과는 아래와 같습니다.)
Error: 403 Forbidden
Error: 404 Not Found
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 main(args: Array<String>) {
val responses = listOf("200 OK", "418 I'm a teapot",
"500 Internal Server Error")
printProblemCounts(responses)
}
위의 코드 역시 forEach에 람다문이 있는데 이 람다에서는 람다 밖에 정의된 clientErrors,serverErrors 을 사용 하고 있습니다. 즉, 코들린에서는 자바와 달린 람다에서 람다 밖 함수에 있는 파이널이 아닌 변수에 접근할 수 있고, 그 변수를 변경할 수 도 있습니다. (람다 안에서 사용하는 외부 변수를 람다가 포획한 변수라고 부릅니다.)
근데 여기서 생각해봐야 할 것이 생깁니다. 일반적으로 함수의 로컬 변수들은 함수가 반환되면서 끝이 납니다. 그런데 함수가 자신의 로컬 변수를 포획한 람다를 반환하거나 다른 변수에 저장한다면 로컬 변수의 생명주기가 함수의 생명주기와 달라질수 있습니다. 이를 유의해야 합니다. ( 더 자세한 내용은 책으로!)
그리고 또 핸들러나 다른 비동기적으로 실행되는 코드를 활용할 경우 함수 호출이 끝난 다음에 로컬 변수가 동작할 수 있습니다. 예를 들어
fun count(button : Button) : Int{
val count = 0;
button.onClick{ count++ }
return count
}
이렇게 될 경우 count 함수는 항상 0일 반환합니다. 이유는 함수 동작이 완료되고 onClick의 람다가 호출되기 때문입니다. 이 문제를 해결하려면 count를 클래스의 프로퍼티로 빼거나 전역 프로퍼티로 만들어야 합니다.
멤버 참조
위에서 람다를 설명할 때 다음과 같은 코드를 봤습니다.
people.maxBy{Person::age}
위와 같이 ::를 사용하는 식을 멤버 참조라고 부른다. 멤버 참조의 문법은 Person 부분이 클래스 ::으로 구분 age 멤버로 구분이 됩니다.
이 멤버 참조는 프로퍼티나 멤소드를 단 하나만 호출 하는 함수 값을 만들어줍니다. 즉 위의 코드는 다음의 람다식을 더 간단하게 표현한 것 입니다.
people.maxBy{person:Person -> person.age}
참조 대상이 함수인지 메소드인지 프로퍼티인지와는 관계없이 멤버 참조 뒤에는 괄호를 넣으면 안된다.
다음은 멤버 참조에 대한 추가 설명들 입니다.
1. 최상위에 선언된 함수나 프로퍼티를 위와 같이 참조할 수 있습니다. 클래스 이름을 생략하고 ::로 참조를 바로 합니다.
fun jeongu() = println("jeongu")
run(::jeongu)
2. 람다가 인자가 여럿인 함수에 작업을 위함하는 경우 람다를 정의하지 않고 직접 위임 함수에 대한 참조를 제공하면 편리합니다.
data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
val action = {person:Person, message:String->
sendEmail(person,message)
}
val nextAction = ::sendEmail
nextAction(Person("jeongu", 99),"Hello jeongu")
}
fun sendEmail(p:Person,m:String){
println("send to ${p.name} message: m")
}
3. 생성자 참조를 사용하면 클래스 생성 작업을 연기하거나 저장할수 있습니다.
data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
val createPerson = ::Person
val p = createPerson("Alice", 29)
println(p)
}
4. 확장 함수도 멤버 함수와 똑같은 방식으로 참조할 수 있습니다.
data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
val p = Person("Alice", 29)
val checkAdult = p.isAdult()
println("${p.name} : ${checkAdult}")
}
fun Person.isAdult() = age>21
'2023년 이전 > kotlin' 카테고리의 다른 글
kotlin - 시퀀스(Sequence) (0) | 2020.03.02 |
---|---|
kotlin - 컬렉션 함수형 API (1) | 2020.03.02 |
kotlin - object 키워드 (0) | 2020.02.25 |
kotlin - 데이터 클래스와 클래스 위임 (0) | 2020.02.25 |
kotlin - 프로퍼티 (간단한 getter,setter 정리 및 추상 프로퍼티) (0) | 2020.02.24 |