클래스의 메서드를 정의할 때는 멤버로 정의할 것인지 확장 함수로 정의할 것인지 결정해야 한다.
물론 두 가지 방법은 비슷하다. 호출하는 방법도, 리플렉션으로 레퍼런싱 하는 방법도 똑같다.
그래서 어떤 방식이 우월하다 할 수 없고 상황에 맞게 사용해야 한다.
일단 멤버와 확장의 큰 차이는 확장은 따로 가져와서 사용해야한다는 것이다. 그래서 확장은 다른 패키지에 위치한다.
그리고 임포트해서 사용하기 대문에 같은 타입에 같은 이름으로 여러개 만들 수 있다. 그러나 같은 이름으로 다른 동작을 하는 확장이 있다면 위험할 수 있기 때문에 멤버 함수로 만들어 사용하는 것이 좋다.
또 확장은 가상이 아니다.
그래서 확장 함수는 컴파일 시점에 정적으로 선택되어 가상 멤버 함수와 다르게 동작한다.
상속을 목적으로 설계된 요소는 확장 함수로 만들면 안 된다.
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
}
중요한 차이점은 확장은 클래스 레퍼런스에서 멤버로 표시되지 않는다는 것이다.
그래서 확장 함수는 어노테이션 프로세서가 따로 처리하지 않는다.
확장 함수는 우리에게 더 많은 자유와 유연성을 준다. 확장 함수는 상속, 어노테이션 처리 등을 지원하지 않고 클래스 내부에 없으므로 약간 혼동을 줄 수도 있다.
API의 필수적인 부분은 멤버로 두는 것이 좋지만, 필수적이지 않다면 확장 함수로 만드는 것이 여러모로 좋다.