[Effective Kotlin] 아이템 15. 리시버를 명시적으로 참조하라

Jimin Lim·2023년 7월 21일
0

Effective Kotlin

목록 보기
15/39
post-thumbnail

아이템 15

리시버를 명시적으로 참조하라

✅ Receiver란?

객체 외부의 람다 코드 블록을 마치 해당 객체 내부에서 사용하는 것처럼 작성할 수 있게 해주는 장치

스코프 내부에 둘 이상의 리시버가 있다면, 명시적으로 나타내는 것이 좋다. 대표적으로 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은 여러 리시버가 중첩되더라도 리시버를 명시적으로 붙이지 않도록 설계되어있다. 하지만 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 마커는 가장 가까운 리시버만을 사용하게 하거나, 명시적으로 외부 리시버를 사용하지 못하게 활용할 수 있다.

reference

https://jaeyeong951.medium.com/kotlin-lambda-with-receiver-5c2cccd8265a

profile
💻 ☕️ 🏝 🍑 🍹 🏊‍♀️

0개의 댓글