안드로이드 개발자 노트
[이펙티브 코틀린] Item15. 리시버를 명시적으로 참조하라 본문
코틀린에서는 특정 객체(타입)을 리시버(수신 객체)로 사용할 수 있습니다.
// block: (T) -> R 일반적인 함수 정의
val block: (Int) -> Int = ...
block(3)
// block: T.() -> R 객체 T를 receiver 로 활용한다.
val block: Int.() -> Int = ...
100.block(3)
이 경우에는 this를 사용해서 리시버를 참조할 수 있습니다.
val sum = fun Int.(other: Int): Int {
return this + other // 키워드 this 를 이용해 리시버 객체에 접근한다.
}
val result = 3.sum(5)
print(result) // 8
this를 명시적으로 사용하지 않고 생략할 수도 있으며, 퀵소트 구현으로 살펴보겠습니다.
// this 생략
fun <T: Comparable<T>> List<T>.quickSort(): List<T> {
if (size < 2) return this
val pivot = first()
val (smaller, bigger) = drop(1)
.partition { it < pivot }
return smaller.quickSort() + pivot + bigger.quickSort()
}
// this 명시
fun <T: Comparable<T>> List<T>.quickSort(): List<T> {
if (this.size < 2) return this
val pivot = this.first()
val (smaller, bigger) = this.drop(1)
.partition { it < pivot }
return smaller.quickSort() + pivot + bigger.quickSort()
}
두 함수의 사용에 차이는 없습니다.
listOf(3, 2, 5, 1, 6).quickSort() // [1, 2, 3, 5, 6]
listOf("C", "D", "A", "B").quickSort() // [A, B, C, D]
여러 개의 리시버
스코프 내부에 둘 이상의 리시버가 있는 경우, 리시버를 명시적으로 나타내면 좋습니다.
apply, with, run 함수를 사용할 때가 대표적인 예입니다.
class Node(val name: String) {
fun makeChild(childName: String) =
create("$name.$childName")
.apply { print("Created ${name}") }
fun create(name: String): Node? = Node(name)
}
fun main() {
val node = Node("parent")
node.makeChild("child")
}
일반적으로 'Created parent.child'가 출력된다고 예상하지만, 실제로는 'Created parent'가 출력됩니다.
이는 apply의 잘못된 사용 예이며, 일반적으로 also 또는 let을 사용하는 것이 명시적으로 리시버를 지정하게 되어 nullable 값을 처리할 때 훨씬 좋은 선택지입니다.
class Node(val name: String) {
fun makeChild(childName: String) =
create("$name.$childName")
.also { print("Create ${it?.name}") }
fun create(name: String): Node? = Node(name)
}
의도한대로 'Created parent.child'를 출력하기 위해선, this를 사용하여 명시적으로 참조해야 합니다.
class Node(val name: String) {
fun makeChild(childName: String) =
create("$name.$childName")
.apply { print("Create ${this?.name}") }
fun create(name: String): Node? = Node(name)
}
리시버가 명확하지 않다면, 명시적으로 리시버를 적어서 이를 명확하게 할 수 있습니다.
이를 레이블이라고 하며, 레이블 없이 리시버를 사용하면 가장 가까운 리시버를 의미합니다.
class Node(val name: String) {
fun makeChild(childName: String) =
create("$name.$childName")
.apply { print("Create ${this?.name} in" +
" ${this@Node.name}") }
fun create(name: String): Node? = Node(name)
}
fun main() {
val node = Node("parent")
node.makeChild("child")
// Create parent.child in parent
}
이렇게 명확하게 작성하면, 코드를 안전하게 사용할 수 있을 뿐만 아니라 어떤 리시버의 함수인지를 명확하게 알 수 있으므로, 가독성도 향상됩니다.
DSL 마커
코틀린 DSL을 사용할 때는 여러 리시버를 가진 요소들이 중첩되더라도, 리시버를 명시적으로 붙이지 않습니다.
원래 그렇게 사용하도록 설계되었기 때문입니다.
그런데 DSL에서는 외부의 함수를 사용하는 것이 위험한 경우가 있습니다.
table {
tr {
td { + "Column1"}
td { + "Column2"}
}
tr {
td { + "Value1"}
td { + "Value2"}
}
}
같은 이름을 가진 tr이라는 함수가 외부 스코프에 있다면, this가 생략된 내 코드가 어떤 tr을 사용하는지 알기 어려울 것입니다.
DslMarker라는 어노테이션을 사용하면 암묵적으로 외부 리시버를 사용하는 것을 막을 수 있습니다.
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class TableDsl { /*...*/ }
fun table(f: TableDsl.() -> Unit) { /*...*/ }
이렇게 하면 암묵적으로 외부 리시버를 사용하는 것이 금지되며, 외부 리시버 함수를 사용하려면, 명시적으로 레이블을 사용해야 합니다.
table {
tr {
td { + "Column1"}
td { + "Column2"}
tr { // 컴파일 오류
td { + "Value1"}
td { + "Value2"}
}
}
}
table {
tr {
td { + "Column1"}
td { + "Column2"}
this@table.tr {
td { + "Value1"}
td { + "Value2"}
}
}
}
정리
- 코드가 짧아진다는 이유만으로 리시버를 제거하면 안된다.
- 여러 개의 리시버가 있는 상황 등에는 리시버를 명시적으로 적어 주는 것이 가독성 면에서 좋다.
- DSL에서 외부 스코프에 있는 리시버를 명시적으로 적게 강제하고 싶다면, DslMarker 메타 어노테이션을 사용하면 된다.
- 추가로, 람다함수에 return 키워드를 사용할 때에도 비슷하게 쓸 수 있다.
'Kotlin > 이펙티브 코틀린' 카테고리의 다른 글
[이펙티브 코틀린] Item16. 프로퍼티는 동작이 아니라 상태를 나타내야 한다. (0) | 2023.10.02 |
---|---|
[이펙티브 코틀린] Item14. 변수 타입이 명확하지 않은 경우 확실하게 지정하라 (0) | 2023.10.01 |
[이펙티브 코틀린] Item13. Unit?을 리턴하지 말라 (0) | 2023.10.01 |