중급: 리시버를 가진 람다 표현식
확장 함수
스코프 함수
리시버를 가진 람다 표현식
클래스와 인터페이스
객체
열린 클래스와 특수 클래스
프로퍼티
널 안정성
라이브러리 및 API
이 장에서는 다른 유형의 함수인 람다 표현식과 함께 리시버를 사용하는 방법과, 이들이 도메인 특화 언어(DSL)를 만드는 데 어떻게 도움이 되는지 배웁니다.
리시버를 가진 람다 표현식
초급 투어에서 람다 표현식을 사용하는 방법을 배웠습니다. 람다 표현식은 리시버를 가질 수도 있습니다. 이 경우 람다 표현식은 리시버를 매번 명시적으로 지정할 필요 없이 리시버의 모든 멤버 함수나 프로퍼티에 접근할 수 있습니다. 이러한 추가적인 참조가 없으면 코드를 더 쉽게 읽고 유지보수할 수 있습니다.
리시버를 가진 람다 표현식은 리시버를 가진 함수 리터럴로도 알려져 있습니다.
리시버를 가진 람다 표현식의 구문은 함수 타입을 정의할 때 다릅니다. 먼저 확장하려는 리시버를 작성합니다. 다음으로 .
을 붙인 다음 나머지 함수 타입 정의를 완료합니다. 예를 들어:
MutableList<Int>.() -> Unit
이 함수 타입은 다음을 가집니다:
MutableList<Int>
를 리시버로 가집니다.- 괄호
()
안에 함수 파라미터가 없습니다. - 반환 값
Unit
이 없습니다.
캔버스에 도형을 그리는 다음 예시를 고려해 보세요:
class Canvas {
fun drawCircle() = println("🟠 Drawing a circle")
fun drawSquare() = println("🟥 Drawing a square")
}
// 리시버를 가진 람다 표현식 정의
fun render(block: Canvas.() -> Unit): Canvas {
val canvas = Canvas()
// 리시버를 가진 람다 표현식 사용
canvas.block()
return canvas
}
fun main() {
render {
drawCircle()
// 🟠 Drawing a circle
drawSquare()
// 🟥 Drawing a square
}
}
이 예시에서:
Canvas
클래스에는 원이나 사각형을 그리는 것을 시뮬레이션하는 두 개의 함수가 있습니다.render()
함수는block
파라미터를 받고Canvas
클래스의 인스턴스를 반환합니다.block
파라미터는Canvas
클래스를 리시버로 가지는 리시버를 가진 람다 표현식입니다.render()
함수는Canvas
클래스의 인스턴스를 생성하고,canvas
인스턴스에block()
람다 표현식을 리시버로 사용하여 호출합니다.main()
함수는block
파라미터에 전달되는 람다 표현식과 함께render()
함수를 호출합니다.render()
함수에 전달된 람다 내부에서 프로그램은Canvas
클래스의 인스턴스에 대해drawCircle()
및drawSquare()
함수를 호출합니다.
리시버를 가진 람다 표현식에서 drawCircle()
및 drawSquare()
함수가 호출되기 때문에, 이 함수들은 Canvas
클래스 내부에 있는 것처럼 직접 호출될 수 있습니다.
리시버를 가진 람다 표현식은 도메인 특화 언어(DSL)를 만들고자 할 때 유용합니다. 리시버를 명시적으로 참조하지 않고도 리시버의 멤버 함수 및 프로퍼티에 접근할 수 있으므로 코드가 더욱 간결해집니다.
이를 시연하기 위해, 메뉴의 항목을 구성하는 예시를 고려해 봅시다. MenuItem
클래스와 메뉴에 항목을 추가하는 item()
함수 및 모든 메뉴 항목의 리스트 items
를 포함하는 Menu
클래스부터 시작하겠습니다:
class MenuItem(val name: String)
class Menu(val name: String) {
val items = mutableListOf<MenuItem>()
fun item(name: String) {
items.add(MenuItem(name))
}
}
메뉴를 빌드하는 menu()
함수에 함수 파라미터 (init
)로 전달된 리시버를 가진 람다 표현식을 시작점으로 사용해 봅시다:
fun menu(name: String, init: Menu.() -> Unit): Menu {
// Menu 클래스의 인스턴스를 생성합니다.
val menu = Menu(name)
// 클래스 인스턴스에 대해 리시버 init()을 가진 람다 표현식을 호출합니다.
menu.init()
return menu
}
이제 DSL을 사용하여 메뉴를 구성하고 메뉴 구조를 콘솔에 출력하는 printMenu()
함수를 만들 수 있습니다:
class MenuItem(val name: String)
class Menu(val name: String) {
val items = mutableListOf<MenuItem>()
fun item(name: String) {
items.add(MenuItem(name))
}
}
fun menu(name: String, init: Menu.() -> Unit): Menu {
val menu = Menu(name)
menu.init()
return menu
}
fun printMenu(menu: Menu) {
println("Menu: ${menu.name}")
menu.items.forEach { println(" Item: ${it.name}") }
}
// DSL 사용
fun main() {
// 메뉴 생성
val mainMenu = menu("Main Menu") {
// 메뉴에 항목 추가
item("Home")
item("Settings")
item("Exit")
}
// 메뉴 출력
printMenu(mainMenu)
// Menu: Main Menu
// Item: Home
// Item: Settings
// Item: Exit
}
보시다시피, 리시버를 가진 람다 표현식을 사용하면 메뉴를 만드는 데 필요한 코드가 크게 단순화됩니다. 람다 표현식은 설정 및 생성뿐만 아니라 구성에도 유용합니다. API, UI 프레임워크 및 구성 빌더를 위한 DSL을 구축하는 데 일반적으로 사용되어 간결한 코드를 생성하므로 기본 코드 구조 및 논리에 더 쉽게 집중할 수 있습니다.
코틀린의 생태계에는 표준 라이브러리의 buildList()
및 buildString()
함수와 같은 이 설계 패턴의 많은 예시가 있습니다.
리시버를 가진 람다 표현식은 코틀린의 **타입 안전 빌더 (Type-safe builders)**와 결합하여 런타임이 아닌 컴파일 시간에 타입 관련 문제를 감지하는 DSL을 만들 수 있습니다. 더 자세히 알아보려면 타입 안전 빌더를 참조하세요.
연습 문제
연습 문제 1
fetchData()
함수는 리시버를 가진 람다 표현식을 받습니다. 람다 표현식을 업데이트하여 append()
함수를 사용하도록 하여 코드의 출력이 Data received - Processed
가 되도록 하세요.
fun fetchData(callback: StringBuilder.() -> Unit) {
val builder = StringBuilder("Data received")
builder.callback()
}
fun main() {
fetchData {
// 여기에 코드를 작성하세요
// Data received - Processed
}
}
예시 솔루션
fun fetchData(callback: StringBuilder.() -> Unit) {
val builder = StringBuilder("Data received")
builder.callback()
}
fun main() {
fetchData {
append(" - Processed")
println(this.toString())
// Data received - Processed
}
}
연습 문제 2
Button
클래스와 ButtonEvent
, Position
데이터 클래스가 있습니다. Button
클래스의 onEvent()
멤버 함수를 트리거하여 더블 클릭 이벤트를 발생시키는 코드를 작성하세요. 코드는 "Double click!"
을 출력해야 합니다.
class Button {
fun onEvent(action: ButtonEvent.() -> Unit) {
// 더블 클릭 이벤트를 시뮬레이션합니다 (우클릭 아님)
val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200))
event.action() // 이벤트 콜백 트리거
}
}
data class ButtonEvent(
val isRightClick: Boolean,
val amount: Int,
val position: Position
)
data class Position(
val x: Int,
val y: Int
)
fun main() {
val button = Button()
button.onEvent {
// 여기에 코드를 작성하세요
// Double click!
}
}
class Button {
fun onEvent(action: ButtonEvent.() -> Unit) {
// 더블 클릭 이벤트를 시뮬레이션합니다 (우클릭 아님)
val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200))
event.action() // 이벤트 콜백 트리거
}
}
data class ButtonEvent(
val isRightClick: Boolean,
val amount: Int,
val position: Position
)
data class Position(
val x: Int,
val y: Int
)
fun main() {
val button = Button()
button.onEvent {
if (!isRightClick && amount == 2) {
println("Double click!")
// Double click!
}
}
}
연습 문제 3
각 요소가 1씩 증가된 정수 리스트의 사본을 생성하는 함수를 작성하세요. List<Int>
를 incremented
함수로 확장하는 제공된 함수 스켈레톤을 사용하세요.
fun List<Int>.incremented(): List<Int> {
val originalList = this
return buildList {
// 여기에 코드를 작성하세요
}
}
fun main() {
val originalList = listOf(1, 2, 3)
val newList = originalList.incremented()
println(newList)
// [2, 3, 4]
}
fun List<Int>.incremented(): List<Int> {
val originalList = this
return buildList {
for (n in originalList) add(n + 1)
}
}
fun main() {
val originalList = listOf(1, 2, 3)
val newList = originalList.incremented()
println(newList)
// [2, 3, 4]
}