참조
Kotlin in Action을 보고 작성하였습니다.
확장 함수는 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수입니다.
package strings
fun String.lastChar() : Char = this.get(this.length - 1)
확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 됩니다. 클래스 이름을 수신 객체 타입(receiver type)이라 부르며, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신 객체(receiver object)라고 부릅니다.
수신 객체 타입은 확장이 정의될 클래스의 타입이며, 수신 객체는 그 클래스에 속한 인스턴스 객체
package strings
// 수신 객체 멤버에 this 없이 접근
fun String.lastChar() : Char = get(length - 1)
확장 함수 내부에서는 일반적인 인스턴스 메서드의 내부에서와 마찬가지로 수신 객체의 메서드나 프로퍼티를 바로 사용할 수 있습니다. 또한 일반 메서드의 본문에서 this를 사용할 때와 마찬가지로 확장 함수 본문에도 this를 쓸 수도 있고, this를 생략할 수도 있습니다.
하지만 확장 함수가 캡슐화를 깨지는 않습니다. 클래스 안에서 정의한 메서드와 달리 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 비공개 멤버(private)나 보호된 멤버(protected)를 사용할 수 없습니다.
확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야만 합니다.
코틀린에서는 클래스를 임포트할 때와 동일한 구문을 사용해 개별 함수를 임포트할 수 있고 혹은 *(스타 임포트)를 사용한 임포트도 작동합니다.
import strings.lastChar
또는
import strings.*
val c = "kotlin".lastChar()
as 키워드를 사용하면 임포트한 클래스나 함수를 다른 이름으로 부를 수도 있습니다.
import strings.lastChar as last
val c = "kotlin".last()
내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메서드입니다. 그래서 확장 함수를 호출해도 다른 어댑터객체나 실행 시점 부가 비용이 들지 않습니다.
이렇기에 자바에서 확장 함수를 호출하려면 정적 메서드를 호출하면서 첫 번째 인자로 수신 객체를 넘기기만 하면 됩니다. 다른 최상위 함수와 마찬가지로 확장 함수가 들어있는 자바 클래스 이름도 확장 함수가 들어있는 파일 이름에 따라 결정됩니다.
// lastChar 확장 함수가 정의된 코틀린 파일의 이름을 StringUtil.kt라고 가정
char c = StringUtilkt.lastChar("Java");
코틀린 메서드 오버라이드는 일반적인 객체 지향의 메서드 오버라이드와 마찬가지로 작성하면 되지만, 확장 함수는 오버라이드 할 수 없습니다.
확장 함수는 클래스의 일부가 아니라 클래스 밖에 선언이 됩니다. 만약 이름과 파라미터가 같은 확장 함수를 부모 클래스와 자식 클래스에 정의해도 실제로는 확장 함수를 호출할 때 수신 객체로 지정한 변수의 정적 타입(컴파일 시)에 의해 어떤 확장 함수가 호출될지 결정되지, 그 변수에 저장된 객체의 동적인 타입(실행 시)에 의해 확장 함수가 결정되지 않습니다.
open class View {}
// Button 클래스가 View 클래스 상속
class Button : View() {}
// 확장 함수 정의
fun View.showOff() = println("view")
fun Button.showOff() = println("Button")
fun main() {
// View 타입 변수에 자식인 Button 객체 대입
val view: View = Button()
// 만약 일반적인 override였다면 동적으로 결정되어 Button의 메서드가 실행되었겠지만
// 확장함수는 정적으로 결정되므로 View클래스의 메서드가 실행
view.showOff()
}
view가 가리키는 객체의 실제 타입이 Button이지만, 이 경우 view의 타입이 View이기 때문에 무조건 View의 확장 함수가 호출됩니다.
확장 함수를 첫 번째 인자가 수신 객체인 정적 자바 메서드로 컴파일한다는 사실을 기억한다면 이런 동작을 쉽게 이해할 수 있습니다.
View view = new Button();
// 위의 kotlin 파일이 Extensions.kt라고 가정
ExtensionsKt.showOff(view);
확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있습니다. 다만 프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 적절한 방법이 없기에(기존 클래스의 인스턴스 객체에 필드를 추가할 방법이 없다) 실제로 확장 프로퍼티는 아무 상태도 가질 수 없습니다.
확장 함수의 경우와 마찬가지로 확장 프로퍼티도 일반적인 프로퍼티와 같은데, 단지 수신 객체 클래스가 추가됐을 뿐입니다. 뒷받침하는 필드(field)가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의를 해야합니다. 마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드를 쓸 수 없습니다.
val String.lastChar: Char
get() = get(length - 1)