Intro
API의 중요하지 않은 부분을 익스텐션으로 추출하는 것을 고려하세요.
클래스에서 final 메서드를 정의할 때, 멤버로 정의할지, 익스텐션 함수로 정의할지 결정해야 합니다.
// 메서드를 멤버로 정의
class Workshop(/*...*/) {
//...
fun makeEvent(date: DateTime): Event = //...
val permalink
get() = "/workshop/$name" //커스텀 get
}
// 메서드를 확장함수로 정의
class Workshop(/(...(/) {
//...
}
fun Workshop.makeEvent(date: DateTime): Event = //...
val Workshop.permalink
get() = "/workshop/$name"
커스텀 get(), set() 은 여기 참고
두 방법은 사용하는 방법이나 reflection을 통한 참조를 하는 것이 매우 유사합니다.
reflection:
런타임 시 자신의 프로그램 구조를 조사할 수 있도록 허용하는 언어와 라이브러리 기능의 집합
예) 런타임 참조를 클래스로 가져오는 것: val myCalss = MyClass::class
참조: https://altongmon.tistory.com/608
fun useWorkshop(workshop: Workshop) {
val event = workshop.makeEvent(date)
val permalink = workshop.permalink
val makeEventRef = Workshop::makeEvent
val permalinkPropRef = Workshop::permalink
}
유사해보이지만, 중요한 차이점이 있고 각자 장단점이 있습니다.
그래서! API의 중요하지 않은 부분을 반드시 익스텐션으로 추출하는 것이 아니라 "고려"하라고 한 것 입니다.
추출할지 말지 현명한 결정을 하기 위해서는 차이점을 이해하는 것이 중요합니다!
차이점 1) 익스텐션은 따로 분리해서 import 해야한다
멤버와 익스텐션을 사용할 때 가장 큰 차이점은 익스텐션은 따로 분리해서 import를 해야한다는 것 입니다.
때문에 익스텐션은 다른 패키지에 있을 수 있고,
- 우리가 직접 멤버를 추가할 수 없을 때
- 데이터와 동작을 분리하도록 설계된 프로젝트
와 같은 상황에서 사용될 수 있습니다.
익스텐션은 가져와야 한다는 사실 덕분에, 같은 유형 & 같은 이름을 가진 많은 익스텐션을 가질 수 있습니다.
하!지!만!
이름은 같지만 동작이 다른 두개의 확장을 갖는 것은 매우 위험합니다.
차이점2) 익스텐션은 가상이 아니다
이 의미는 파생 클래스에서 재정의할 수 없다는 뜻입니다.
호출할 확장 함수는 컴파일 중에 정적으로(statically) 선택됩니다.
따라서 상속을 위해 설계된 element에 대해서는 익스텐션을 사용하면 안됩니다.
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()) // 뭘까요?
print(D().foo()) // d
print((D() as C).foo()) // 뭘까요?
답) c, c
이 동작은 익스텐션 함수가 일반 함수로 컴파일된다는 것을 확실하게 이해해야 합니다.
익스텐션의 receiver가 첫번째 argument로 배치됩니다.
fun foo(`this$receiver`: C) = "c"
fun foo(`this$receiver`: D) = "d"
fun main() { ... }
이 사실의 또 다른 결과는 클래스가 아니라 type에 대한 익스텐션을 정의한다는 것입니다.
예를 들어, nullable 또는 제네릭 유형의 구체적인 대체에 대한 익스텐션을 정의할 수 있습니다.
inline fun CharSequence?.isNullOrBlank(): Boolean {
contract {
returns(false) implies (this@isNullOrBlank != null)
//false를 반환하는 경우, isNullOrEmpty 블럭은 null이 아니라는 정보를 컴파일러에게 전달
}
return this == null || this.isBlack()
}
public fun Iterable<Int>.sum(): Int {
var sum: Int = 0
for (element in this) {
sum += element
}
return sum
}
cf) Contract 함수에 대한 이해
Contracts를 사용하는경우 현재는 True, False, Null값만 반환할 수 있고 위 예제는 false값을 리턴합니다.
fun getString(): String? = null
fun some() {
val str: String? = getString()
if (!str.isNullOrEmpty()) {
println(str.length) // compile error
}
}
예제코드처럼 작성하면 str은 null 체크를 함에도 불구하고, 컴파일러는 str 선언당시 nullable 하다는 정보만 가지고 error를 발생 시킵니다. 현재 코틀린(1.2.60) 정식버전에서는 이런 스마트 캐스팅을 지원하지 않습니다.
contract는 이런 불편함을 해결할 수 있습니다.
차이점 3) 익스텐션이 클래스 참조의 멤버로 나열되지 않는다
클래스 참조의 멤버로 리스팅되지 않기 때문에,
주석처리를 사용해서 클래스를 처리할 때(?! 무슨말이징) 익스텐션 함수로 처리되어야 하는 요소를 추출할 수 없습니다.
반면 필수가 아닌 요소를 익스텐션으로 사용하면, 프로세서에서 보이는 것을 걱정하지 않아도 됩니다.
Summary
요약하자면, 익스텐션은 우리에게 더 많은 자유와 유연성을 제공합니다.
상속, 주석처리가 지원되지 않기 때문에, 클래스에 존재하지 않다고 혼동될 수 있습니다.
API에서 필수 부분은 구성원으로 유지해야 하지만, 필수가 아닌 부분은 확장으로 뺄만한 충분한 이유가 있습니다.
댓글