메서드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티

기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 여러 편리한 기능을 사용할 수 있게끔 하는 것이 확장 함수다.

다음은 어떤 문자열의 마지막 문자를 돌려주는 메서드다.

fun String.lastChar() : Char = this.get(this.length-1)

확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다.

클래스 이름을 수신 객체 타입 이라 부르며, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신 객체 라고 부른다.

위 함수에서 수신 객체 타입은 String 이고, 수신 객체는 this 다.

출력은 이런 식으로 한다.

-> println("Kotlin".lastChar())
n

그리고 일반 메서드와 마찬가지로 확장 함수 본문에서도 this를 생략할 수 있다.

fun String.lastChar(): Char = get(length -1)

임포트와 확장 함수

확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야만 한다.

import strings.lastChar 또는 import strings.*

val c = "Kotlin".lastChar()

그리고 as 키워드를 사용하면 임포트한 클래스나 함수를 다른 이름으로 부를 수 있다.

import strings.lastChar as last

val c = "Kotlin".last()

임포트할 때 이름을 바꾸는 것이 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법이다.

자바에서 확장 함수 호출

확장 함수를 StringUtil.kt 파일에 정의했다면 다음과 같이 호출할 수 있다.

/* 자바 */
char c = StringUtilKt.lastChar("Java");

확장 함수로 유틸리티 함수 정의

확장 함수로 joinToString 함수의 최종 버전을 만들어보자.

fun <T> Collection<T>.joinToString ( // Collection<T> 에 대한 확장 함수를 선언한다.
	separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) : String {
	val result = StringBuilder(prefix)
    
    for((index, element) in this.withIndex()) // this는 수신 객체를 가리킨다. 여기서는 T 타입의 원소로 이뤄진 컬렉션이다.
    	if(index > 0) result.append(separator)
        result.append(element)
}

result.append(postfix)
return result.toString()

}

-> val list = listOf(1, 2, 3)
-> println(list.joinToString(separator = "; ",
... prefix = "(", postfix = ") "))

(1; 2; 3)

이제 joinToString을 마치 클래스의 멤버인 것처럼 호출할 수 있다.

-> val list = arrayListOf(1, 2, 3)
-> println(list.joinToString(" "))
1 2 3

확장 함수는 단지 정적 메서드 호출에 대한 문법적인 편의일 뿐이다. 그래서 클래스가 아닌 더 구체적인 타입을 수신 객체 타입으로 지정할 수도 있다.

fun Collection<String>.join (
	separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) = joinToString(separator,prefix,postfix)

-> println(listOf("one", "two", "eight").join(" "))
one two eight

이 함수를 객체의 리스트에 대해 호출할 수는 없다.

-> listOf(1, 2, 8).join()
Error: Type mismatch: inferred type is List<Int> but Collection<String> was expected.

확장 함수는 오버라이드할 수 없다.

View와 그 하위 클래스인 Button이 있는데, Button이 상위 클래스의 Click 함수를 오버라이드하는 경우를 생각해보자.

open class View{
	open fun click() = println("View clicked")
}

class Button: View() { // Button은 View를 확장한다.
	override fun click() = println("Button clicked")
}

하지만 확장은 이런 식으로 작동하지 않는다.

확장 함수는 클래스의 일부가 아니다. 확장 함수는 클래스 밖에 선언된다. 이름과 파라미터가 완전히 같은 확장 함수를 기반 클래스와 하위 클래스에 대해 정의해도 실제로는 확장 함수를 호출할 때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정되지, 그 변수에 저장된 객체의 동적인 타입에 의해 확장 함수가 결정되지 않는다.

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

-> val view: View = Button()
-> view.showOff() // 확장 함수는 정적으로 결정된다.
I'm a view! // 그래서 Button 객체로 오버라이드 했지만 view의 타입이 View이기 때문에 I'm a view! 가 그대로 출력되는 모습.


/* 자바 */
-> View view = new Button();
-> ExtensionKt.showOff(view);
I'm a view!

어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다(멤버 함수의 우선순위가 더 높다). 클래스의 API를 변경할 경우 항상 이를 염두에 둬야 한다. 여러분이 코드 소유권을 가진 클래스에 대한 확장 함수를 정의해서 사용하는 외부 클라이언트 프로젝트가 있다고 하자. 그 확장 함수와 이름과 시그니처가 같은 멤버 함수를 여러분의 클래스 내부에 추가하면 클라이언트 프로젝트를 재컴파일한 순간부터 그 클라이언트는 확장 함수가 아닌 새로 추가된 멤버 함수를 사용하게 된다.
무슨 소리일까?..기술부채로..

확장 프로퍼티

val String.lastChar: Char
	get() = get(length - 1)

확장 프로퍼티도 일반적인 프로퍼티와 같은데, 단지 수신 객체 클래스가 추가됐을 뿐이다. 뒷받침하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의를 해야 한다.

마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.

var StringBuilder.lastChar: Char
	get() = get (length - 1) // 프로퍼티 게터
    set(value: Char) {
    	this.setCharAt(length - 1, value) // 프로퍼티 세터
    }

위 확장 프로퍼티를 사용하는 방법은 다음과 같다.

-> println("Kotlin".lastChar)
n

-> val sb = StringBuilder("Kotlin?")
-> sb.lastChar = '!'
-> println(sb)
Kotlin!

좋은 웹페이지 즐겨찾기