들어가며
코틀린은 이미 정의된 클래스를 확장 시킬 수 있는 기능이 있다. 예를 들어서 아래와 같이 String 클래스에 원하는 메서드를 추가 할 수 있다.
fun String.getFirst() = this[0]
println("hello".getFirst())
//print 'h'
원리
원리는 생각보다 간단하다. 자바에서 static을 사용하여 기존 클래스를 입력받아 처리하는 메서드를 생성함으로써 사용자 입장에서는 기존의 클래스를 확장시킨 것처럼 보이게 하는 것이다.
public static String getFisrt(String string){ return string[0];}
Question?
그렇다면 만약 상속 관계에 있는 두 개의 클래스를 이용하여 아래와 같이 코드를 작성하면 어떤 문자열이 출력될까?
open class Parent
class Child: Parent()
fun Parent.foo() = "parent"
fun Child.foo() = "child"
fun main(args: Array<String>) {
val parent: Parent = Child()
println(parent.foo())
// 여기서 출력되는건 무엇일까?
}
Answer & Why
원리를 알고있다면 추론하는 것은 간단하다. 위 코드가 자바로 변환 되었다고 생각해보자. static을 이용하므로 아래와 같이 변환 되었을 것이다.
//... 클래스 정의는 생략
public static String foo(Parent parent){return "parent";)
public static String foo(Child child){return "child";)
public static void main(String[] args){
Parent parent=new Child();
System.out.println(foo(parent));
}
자바 공부를 열심히 했다면 결과는 "parent"가 출력되었을 것이라고 바로 알았을 것이다. 자바는 오버로딩을 통해서 어규먼트와 파라미터가 일치하는 함수를 호출하기 때문이다. 위 코드를 보면 오버라이딩을 통해서 "child"가 출력될수 있다고 착각할 수있다. 그러나 확장함수는 인스턴스의 실제 값과는 관련없이 클래스에만 선언되기 때문에 함수를 호출할 때 인스턴스의 클래스 종류만을 따른다.
Question2 : Member vs Extension
자 그럼 만약 확장함수를 기존 클래스의 멤버 메소드를 오버라이딩 하는 것처럼 사용하면 어떻게 될까?
class A{
fun foo()="member"
}
fun A.foo()="extension"
println(A().foo()) //??
Answer & Why
정답은 "member"를 출력한다. 왜냐하면 코틀린은 확장함수가 멤버 함수를 오버라이딩 하는 것을 허용하지 않는다. 왜 그럴까? 만약 반대로 허용한다고 생각해보자, 코드를 작성 할때 전역에서 확장함수로 Array.get()을 새로 정의 했다고 하자. 추후 다른 사람이 이 파일에 새로운 코드를 추가한다고 했을 때, get()을 본래의 의도로 사용하여 작성한다면 코드는 버그 투성이가 될 것이고 이를 인지하고 본래의 함수를 사용하고자 해도 확장함수로 정의된 이상 불가능하다.
하지만 코틀린은 확장함수로 오버로딩을 하는 것은 허용한다. 이는 기존의 멤버 메소드에 영향을 끼치지 않기 때문이다.
class A{
fun foo()="member"
}
fun A.foo(i:Int)="extension $i"
println(A().foo(2)) //extension 2