ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 15장 - 리시버를 명시적으로 참조하라
    Kotlin/Effective Kotlin 요약 2023. 2. 13. 00:01

    This

    대표적으로 함수와 프로퍼티를 지역 또는 톱 레벨 변수가 아닌 다른 리시버로부터 가져온다는 것을 나타낼 때가 있습니다.

    예를 들어 클래스의 메서드라는 것을 나타내기 위한 this가 있습니다.

    class User: Person(){
    	private var beersDrunk: Int = 0
    	fun drinkBeers(num: Int){
    		this.beersDrunk += num    
    	}
    }

     

    명시적으로 표현하지 않음 vs 명시적으로 표현

    fun <T: Comparable<T>> List<T>.quickSort(): List<T>{
    	if(size < 2) return this
    	...
    }

    명시적이지 않은 구현입니다.

     

    fun <T: Comparable<T>> List<T>.quickSort(): List<T>{
    	if(this.size < 2) return this
    	...
    }

    명시적인 구현입니다.

     

    두 함수의 사용에 차이는 없지만 명시적인 경우에가 훨씬 이해하기 편합니다.

     

    여러 개의 리시버

    스코프 내부에 둘 이상의 리시버가 있는 경우, 리시버를 명시적으로 나타내면 좋습니다.

    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가 출력됩니다.

     

    조금 더 이해할 수 있게 앞에 명시적으로 리시버를 붙여 줍니다.

    class Node(val name: String){
        fun makeChild(childName: String) =
            create("$name.$childName").apply {print("Created ${this.name}")} //컴파일 에러 발생
        fun create(name: String): Node? = Node(name)
    }
    
    fun main() {
        val node = Node("parent")
        node.makeChild("child")
    }

    apply 함수 내부에서 this의 타입이 Node? 라서 이를 직접 사용할 수 없어 컴파일 에러가 발생합니다.

    사실 이는 apply의 잘못된 사용 예입니다.

    also함수와 파라미터 name을 사용하면 문제 자체가 발생하지 않습니다.

    also를 사용하면 이전과 마찬가지로 명시적으로 리시버를 지정하게 됩니다.

     

    일반적으로 also 또는 let을 사용하는 것이 nullable 값을 처리할 때 훨씬 좋은 선택지입니다.

    class Node(val name: String){
        fun makeChild(childName: String) =
            create("$name.$childName").also {print("Created ${it?.name}")}
        fun create(name: String): Node? = Node(name)
    }

     

    리시버가 명확하지 않다면 명시적으로 리시버를 적어서 이를 명확하게 하는게 좋습니다.

    레이블 없이 리시버를 사용하면, 가장 가까운 리시버를 의미합니다.

    외부에 있는 리시버를 사용하려면 레이블을 사용해야 합니다. (외부에 있는 for문을 break 하는 것과 유사)

    class Node(val name: String){
        fun makeChild(childName: String) =
            create("$name.$childName").apply {print("Created ${this?.name} in"
            + " ${this@Node.name}")}
        fun create(name: String): Node? = Node(name)
    }

    어떤 리시버를 사용하는지 의미가 훨씬 명확해졌습니다.

    이렇게 하면 코드를 안전하게 사용할 수 있으며 가독성도 향상됩니다.

     

    DSL 마커

    DSL을 사용할 때 여러 리시버를 가진 요소들이 중첩되어도, 리시버를 명시적으로 붙이지 않습니다.

    하지만 DSL에서 외부 함수를 사용하는 것이 위험한 경우가 있습니다.

    예로 간단하게 HTML table 요소를 만드는 HTML DSL을 생각해 볼 수 있습니다.

     

    table{
    	tr{
    		td { +"Column 1 "}
    		td { +"Column 2 "}
    	}
        
    	tr{
    		td { +"Value 1 "}
    		td { +"Value 2 "}
    	}
    }

    기본적으로 모든 스코프에서 외부 스코프에 있는 리시버의 ㄹ메서드를 사용할 수 있습니다.

    하지만 이렇게 하면 코드에 문제가 발생합니다.

     

    table{
    	tr{
    		td { +"Column 1 "}
    		td { +"Column 2 "}
                
    		tr{
    			td { +"Value 1 "}
    			td { +"Value 2 "}
    		}
    	}
    
    }

    이러한 잘못된 사용을 막으려면, 암묵적으로 외부 리시버를 사용하는 것을 막는 DslMarker라는 메타 어노테이션을 사용해야 합니다.

     

    결론

    짧게 적을 수 있다는 이유만으로 리시버를 제거하지 말아야 합니다.

    여러 개의 리시버가 있는 상황 등에서 리시버를 명시적으로 적어 주는 곳이 좋습니다.

     

    댓글

Designed by Tistory.