BottleH Blog

Kotlin in Action - 5장 람다를 사용한 프로그래밍

    Tags

  • Kotlin
Kotlin in Action - 5장 람다를 사용한 프로그래밍 thumbnail
  • 람다는 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 의미

📖 5.1 람다식과 멤버 참조

🔖 5.1.1 람다 소개: 코드 블록을 값으로 다루기

button.setOnClickListener(object: OnClickListener { override fun onClick(v: View) { println("I was clicked!") } }) button.setOnClickListener { println("I was clicked!") }
  • 람다를 메서드가 하나뿐인 익명 객체 대신 사용할 수 있다.

🔖 5.1.2 람다와 컬렉션

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) } people.maxByOrNull { it.age }
  • maxByOrNull 함수를 이용하면 편한다.
  • 람다가 인자를 하나만 받고 그 인자에 구체적인 이름을 붙이고 싶지 않기 때문에 it이라는 암시적 이름을 사용한다.
  • 멤버 참조 또한 사용할 수 있다.

🔖 5.1.3 람다식의 문법

fun main() { val sum = {x: Int, y: Int -> x + y} println(sum) }
  • 코틀린 람다식은 중괄호로 둘러싸여 있다.
  • 화살표가 인자 목록과 람다 본문을 구분해준다.
fun main() { { println(42) } }
  • 람다식을 직접 호출해도 된다.
  • 별로 쓸모가 없어 직접 실행하는 편이 낫다.
fun main() { run{ println(42) } }
  • run은 인자로 받은 람다를 실행해 주는 라이브러리 함수
people.maxByOrNull({p: Person -> p.age })
  • 구분자가 너무 많이 쓰여서 가독성이 덜어진다.
  • 컴파일러가 문맥으로부터 유추할 수 있는 인자 타입을 굳이 적을 필요가 없다.
  • 인자가 하나뿐인 경우 이름을 붙이지 않아도 된다.
people.maxByOrNull() {p: Person -> p.age }
  • 코틀린에는 함수 호출 시 맨 뒤에 있는 인자가 람다식이라면 그 람다를 괄호 밖으로 빼낼 수 있다.
    • 문법적 관습
people.maxByOrNull {p: Person -> p.age }
  • 람다가 어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 썼다면 호출시 빈 괄호를 없애도 된다.
fun main() { val people = listOf(Person("Dmitry", 4), Person("Eve", 4)) val names = people.joinToString( separator = " ", transform = { p: Person -> p.name } ) println(names) } people.joinToString(" ") { p: Person -> p.name }
  • 굉장히 간결하게 변한다.
people.maxByOrNull { p: Person -> p.age } people.maxByOrNull { p -> p.age } // 파라미터 타입을 컴파일러가 추론
  • 로컬 변수처럼 컴파일러는 람다 파라미터의 타입도 추론할 수 있다.
  • it을 너무 남용하는 것보다는 파라미터를 명시하는 편이 낫다.
val getAge = { p: Person -> p.age } people.maxByOrNull { getAge}
  • 람다를 변수에 저장할 때는 파라미터의 타입을 추론할 문백이 존재하지 않는다.
fun main() { val sum = { x: Int, y: Int -> println("Computing $x and $y") x + y } }
  • 본문이 여러 줄로 이뤄진 경우 본문의 맨 마지막에 있는 식이 람다의 결괏값이 된다.
  • 명시적인 return이 필요하지 않다.

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

fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) { messages.forEach { println("$prefix $it") } }
  • forEach 람다는 자신을 둘러싼 영역에 정의된 prefix 변수와 다른 변수에 접근할 수 있다.
    • 파일 영역에 이를 때까지 자신을 둘러싼 영역의 변수를 참조할 수 있다.
fun printProblemCounts(responses: Collection<String>) { var clientErrors = 0 var serverErrors = 0 responses.forEach { if (it.startsWith("4")) { clientErrors++ } else if (it.startsWith("5")) { serverErrors++ } } }
  • 코틀린과 자바 람다의 다른 점 중 한가지는 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다는 점이다.
  • 람다 안에서 접근할 수 있는 외부 변수를 람다가 캡처한 변수라고 부른다.
  • 기본적으로 함수 안에 정의된 로컬 변수의 생명주기는 함수가 반환되면 끝난다.
  • 파이널 변수 캡처 -> 람다 코드를 변수 값과 함께 저장
  • 파이널이 아닌 변수를 캡처 -> 변수를 특별한 래퍼로 감싸서 나중에 변경하거나 읽을 수 있게 한 다음, 래퍼에 대한 참조를 람다 코드와 함께 저장

🔖 5.1.5 멤버 참조

val getAge = Person::age
  • ::을 사용하는 식을 멤버 참조라고 부른다.
    • 한 메서드를 호출하거나 한 프로퍼티에 접근하는 함수 값을 만들어준다.
  • 참조 대상이 함수인지 프로퍼티인지와는 관계없이 멤버 참조 뒤에는 괄호를 넣으면 안 된다.
fun salute() = println("Salute") fun main() { run { ::salute } }
  • 최상위에 선언된 함수나 프로퍼티를 참조할 수도 있다.
fun main() { val createPerson = ::Person }
  • 생성자 참조를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다.
  • 확장 함수도 똑같은 방식으로 참조할 수 있다.

🔖 5.1.6 값과 엮인 호출 가능 참조

fun main() { val seb = Person("Sebastian", 26) val personsAgeFunction = Person::age // 사람이 주어지면 나이를 돌려주는 멤버 참조 println(personsAgeFunction(seb)) // 사람을 인자로 받음 val sebsAgeFunction = seb::age // 특정 사람의 나이를 돌려주는, 값과 엮인 호출 가능 참조 println(sebsAgeFunction) // 파라미터 지정하지 않아도 됨 }
  • 특정 객체 인스턴스에 대한 메서드 호출에 대한 참조를 만들 수 있다.

📖 5.2 자바의 함수형 인터페이스 사용: 단일 추상 메서드

함수형 인터페이스 or 단일 추상 메서드 인터페이스

  • 인터페이스 안에 추상 메서드가 단 하나

🔖 5.2.1 람다를 자바 메서드의 파라미터로 전달

void postponeComputation(int delay, Runnable computation) {} postponeComputation(1000) { println(42) }
  • 컴파일러는 자동으로 Runnable의 인스턴스로 변환해준다.
    • 익명 클래스의 인스턴스를 만들어줌
postponeComputation(1000, object : Runnable) { override fun run() { println(42) } }
  • 명시적으로 선언하면 호출할 때마다 새 인스턴스가 생기지만 람다를 사용하면 람다에 해당하는 익명 객체가 재사용된다.
  • 람다를 inline 표시가 돼 있는 코틀린 함수에 전달하면 익명 클래스가 생성되지 않는다.

🔖 5.2.2 SAM 변환: 람다를 함수형 인터페이스로 명시적 변환

fun createAllDoneRunnable(): Runnable { return Runnable { println("All done!") } } fun main() { createAllDoneRunnable().run() }
  • SAM 생성자는 컴파일러가 생성한 함수로 람다를 단일 추상 메서드 인터페이스의 인스턴스로 명시적으로 변환
  • SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다.
  • SAM 생성자는 하나의 인자만을 받아 함수형 인터페이스를 구현하는 클래스의 인스턴스를 반환
val listener = OnClickListener {view -> // 람다를 사용해 SAM 생성자 호출 val text = when (view.id) { button1.id -> "First button" button2.id -> "Second button" else -> "Unknown button" } toast(text) }
  • 값을 반환할 때 외에 람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장해야 하는 경우에도 SAM 생성자 사용
  • 람다에는 익명 객체와 달리 인스턴스 자신을 가리키는 this가 없다.
    • 람다 안에서 this는 그 람다를 둘러싼 클래스의 인스턴스를 가리킨다.
  • SAM 변환을 컴파일로가 자동으로 수행할 수 있지만 가끔 오버로드한 메서드 중에서 어떤 타입의 메서드를 선택해 람다를 변환해 넘겨줘야 할지 모호한 때에는 명시적으로 SAM 생성자 적용하면 된다.

📖 5.3 코틀린에서 SAM 인터페이스 정의: fun interface

fun interface IntCondition { fun check(i: Int): Boolean fun checkString(s: String) = check(s.toInt()) fun checkChar(c: Char) = check(c.digitToInt()) }
  • fun interface라고 정의된 타입의 파라미터를 받는 함수가 있을 때 람다 구현이나 람다에 대한 참조를 직접 넘길 수 있다.
fun checkCondition(i: Int, condition: IntCondition): Boolean { return condition.check(i) } fun main() { checkCondition(1) { it % 2 != 0} // 람다 직접 사용 val isOdd: (Int) -> Boolean = { it % 2 != 0 } checkCondition(1, isOdd) // 시그니처가 일치하는 람다에 대한 참조 사용 }
  • 함수형 타입 시그니처로 표현할 수 없는 연산이나 더 복잡한 계약을 표현하려면 함수형 인터페이스가 좋은 선택

📖 5.4 수신 객체 지정 람다: with, apply, also

  • 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메서드를 호출할 수 있게 하는 것

🔖 5.4.1 with 함수

fun alphabet(): String { val result = StringBuilder() for (letter in 'A'..'Z') { result.append(letter) } result.append("\nNow I know the alphabet!") return result.toString() }
  • 매번 result라는 이름을 반복 사용
fun alphabet(): String { val stringBuilder = StringBuilder() return with(stringBuilder) { // 메서드를 호출하려는 수신 객체를 지정 for (letter in 'A'..'Z') { this.append(letter) } this.append("\nNow I know the alphabet!") this.toString() } }
  • 위 예시의 파라미터는 두개다.
    • stringBuilder와 람다
  • with 함수는 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체로 만든다.
fun alphabet(): String { val stringBuilder = StringBuilder() return with(stringBuilder) { // 메서드를 호출하려는 수신 객체를 지정 for (letter in 'A'..'Z') { append(letter) } append("\nNow I know the alphabet!") toString() } }
  • 람다 안에서는 명시적인 this 참조를 사용해 그 수신 객체에 접근할 수 있다.
    • this를 없애고 메서드나 프로퍼티 이름만 사용해 접근 가능
fun alphabet() = with(StringBuilder()) { for (letter in 'A'..'Z') { append(letter) } append("\nNow I know the alphabet!") toString() }
  • 식을 바로 반환

🔖 5.4.2 apply 함수

fun alphabet() = StringBuilder().apply { for (letter in 'A'..'Z') { append(letter) } append("\nNow I know the alphabet!") }.toString()
  • apply는 항상 자신에 전달된 객체(수신 객체)를 반환한다.
fun createViewWithCustomAttributes(context: Context) = TextView(context).apply { text = "Sample Text" textSize = 20.0 setPadding(10, 0, 0, 0) }
  • apply를 객체 초기화에 활용할 수 있다.
  • java의 builder와 비슷하다.
fun alphabet() = buildString { for (letter in 'A'..'Z') { append(letter) } append("\nNow I know the alphabet!") }
  • buildString 함수는 StringBuilder를 활용해 String을 만드는 경우 사용할 수 있는 우아한 해법
  • buildList, buildSet, buildMap

🔖 5.4.3 객체에 추가 작업 수행: also

fun main() { val fruits = listOf("Apple", "Banana", "Cherry") val uppercaseFruits = mutableListOf<String>() val reversedLongFruits = fruits .map { it.uppercase() } .also { uppercaseFruits.addAll(it) } .filter { it.length > 5 } .also { println(it) } .reversed() }
  • 수신 객체에 대해 어떤 동작을 수행한 후 수신 객체 반환
  • also의 람다 안에서는 수신 객체를 인자로 참조
    • 디폴트로 it
  • 원래의 수신 객체를 인자로 받는 동작을 실행할 때 also가 유용하다.
Written by@BottleH
Back-End Developer

GitHub