리시버를 명시적으로 참조하라
객체 외부의 람다 코드 블록을 마치 해당 객체 내부에서 사용하는 것처럼 작성할 수 있게 해주는 장치
스코프 내부에 둘 이상의 리시버가 있다면, 명시적으로 나타내는 것이 좋다. 대표적으로 apply, with, run 함수를 사용할 때를 예로 들 수 있다.
class Node(val name: String) {
fun makeChild(childName: String) =
create("$name.$childName") //Node("Parent.child")
.apply {
print("Created $name") //Created Parent
}
fun create(name: String): Node? = Node(name)
}
fun main() {
val node = Node("Parent")
node.makeChild("child")
}
위와 같이 작성한다면 Created Parent를 얻을 수 있고, apply 내부에서 Created ${this?.name}
로 해야 원하는 답을 얻을 수 있다.
apply의 잘못된 예이며, also 나 let을 사용해 리시버를 지정하면 더 좋은 코드로 작성할 수 있다.
class Node(val name: String) {
fun makeChild(childName: String) =
create("$name.$childName")
.also {
print("Create ${it?.name}")
}
fun create(name: String): Node? = Node(name)
}
fun main() {
val node = Node("parent")
node.makeChild("child")
}
apply를 사용해 this 라는 리시버를 사용하겠다면, this@Node.name 와 같이 레이블을 표현할 수 있다. (단, 레이블을 나타내지 않는다면 가장 가까운 리시버를 사용한다.)
DSL은 여러 리시버가 중첩되더라도 리시버를 명시적으로 붙이지 않도록 설계되어있다. 하지만 DSL에서 외부의 함수를 사용하는 것이 위험한 경우가 있다.
//Bad
table {
tr {
td {+"Column 1"}
td {+"Column 2"}
tr {
td {+"Value 1"}
td {+"Value 2"}
}
}
}
//Good
table {
tr {
td {+"Column 1"}
td {+"Column 2"}
this@table.tr { // ok
td {+"Value 1"}
td {+"Value 2"}
}
}
}
위와 같이 작성하면, 모든 스코프에서 외부 스코프에 있는 메서드를 사용할 수 있어 문제가 발생할 수 있다. (동일한 이름의 메서드가 내부에도 정의되어 있다거나..)
따라서 DSLMarker라는 메터 어노테이션(어노테이션을 위한 어노테이션)을 사용할 수 있다.
@DslMarker
annotation class HtmlDsl
fun table(f: TableDsl.() -> Unit) { /*...*/ }
@HtmlDsl
class TableDsl { /*...*/ }
DSL 마커는 가장 가까운 리시버만을 사용하게 하거나, 명시적으로 외부 리시버를 사용하지 못하게 활용할 수 있다.
https://jaeyeong951.medium.com/kotlin-lambda-with-receiver-5c2cccd8265a