이펙티브 코틀린 Item 43: API의 필수적이지 않는 부분을 확장 함수로 추출하라

woga·2023년 11월 19일
0

코틀린 공부

목록 보기
54/54
post-thumbnail

클래스의 메서드를 정의할 때는 멤버로 정의할 것인지 확장 함수로 정의할 것인지 결정해야 한다.

물론 두 가지 방법은 비슷하다. 호출하는 방법도, 리플렉션으로 레퍼런싱 하는 방법도 똑같다.
그래서 어떤 방식이 우월하다 할 수 없고 상황에 맞게 사용해야 한다.

일단 멤버와 확장의 큰 차이는 확장은 따로 가져와서 사용해야한다는 것이다. 그래서 확장은 다른 패키지에 위치한다.
그리고 임포트해서 사용하기 대문에 같은 타입에 같은 이름으로 여러개 만들 수 있다. 그러나 같은 이름으로 다른 동작을 하는 확장이 있다면 위험할 수 있기 때문에 멤버 함수로 만들어 사용하는 것이 좋다.

확장은 가상이 아니다.
그래서 확장 함수는 컴파일 시점에 정적으로 선택되어 가상 멤버 함수와 다르게 동작한다.

상속을 목적으로 설계된 요소는 확장 함수로 만들면 안 된다.

open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"

fun main() {
	val d = D()
    print(d.foo()) // d
    val c: C = d
    print(c.foo()) // c
    
    print(D().foo()) // d
    print(D() as C).foo()) //c
}

이 차이는 확장 함수가 첫 번째 아규먼트로 리시버가 들어가는 일반 함수로 컴파일 되기 때문에 발생되는 결과이다.

fun foo('this$receiver': C) = "c"
fun foo('this$receiver': D) = "d"

fun main() {
	val d = D()
    print(foo(d)) // d
    val c: C = d
    print(foo(c)) // c
    
    print(foo(D())) // d
    print(foo(D() as C)) // c
}

추가로 확장 함수는 클래스가 아닌 타입에 정의하는 것이다. 그래서 nullable 또는 구체적인 제네릭 타입에도 확장 함수를 정의할 수 있다.

inline fun CharSequence?.isNullOrBlank(): Boolean {
	contract {
    	return(false) implies (this@isNullOrBlank != null)
    }
    
    return this == null || this.isBlank()
}

public fun Iterable<Int>.sum(): Int {
	var sum: Int = 0
    for (element in this) {
    	sum += element
    }
    return sum
}

중요한 차이점은 확장은 클래스 레퍼런스에서 멤버로 표시되지 않는다는 것이다.
그래서 확장 함수는 어노테이션 프로세서가 따로 처리하지 않는다.

정리

  • 확장 함수는 읽어 들여야 한다.
  • 확장 함수는 virtual이 아니다.
  • 멤버는 높은 우선 순위를 갖는다.
  • 확장 함수는 클래스 위가 아니라 타입 위에 만들어진다.
  • 확장 함수는 클래스 레퍼런스에 나오지 않는다.

확장 함수는 우리에게 더 많은 자유와 유연성을 준다. 확장 함수는 상속, 어노테이션 처리 등을 지원하지 않고 클래스 내부에 없으므로 약간 혼동을 줄 수도 있다.
API의 필수적인 부분은 멤버로 두는 것이 좋지만, 필수적이지 않다면 확장 함수로 만드는 것이 여러모로 좋다.

profile
와니와니와니와니 당근당근

0개의 댓글