2023. 3. 26. 16:36ㆍProgramming Languages/Kotlin
확장 함수
어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수이다.
package strings
fun String.lastChar(): Char = this.get(this.length - 1)
확장 함수를 만들려면 추가하려는 함수 앞에 그 함수가 확장할 클래스의 이름을 덧붙이면 된다. 이때, 클래스 이름을 수신 객체 타입(receiver type), 확장 함수가 호출되는 대상이 수신 객체(receiver object)라고 부른다. 위의 경우 String
이 수신 객체 타입, this
가 수신 객체가 된다.
위 함수를 사용하는 코드를 보자.
println("Kotlin".lastChar()) // n
이때 수신 객체 타입은 String
이고 수신 객체는 Kotlin
이라는 문자열이 된다. 또한 확장 함수를 작성할 때 메소드의 본문에서 this
를 생략하는 것이 가능하다.
fun String.lastChar(): Char = get(length - 1)
확장 함수 내부에서는 일반적인 인스턴스 메소드와 마찬가지로 수신 객체의 메소드나 프로퍼티를 사용할 수 있다. 하지만 확장 함수의 캡슐화는 깨지지 않는다. 왜냐하면 확장 함수 안에서는 private
멤버나 protected
멤버를 사용할 수 없기 때문이다.
1. 임포트와 확장 함수
확장 함수도 사용하려면 그 함수를 다른 클래스나 함수처럼 임포트해야 한다.
import strings.lastChar
val c = "Kotlin".lastChar()
// *를 사용한 임포트도 가능하다.
import strings.*
val c = "Kotlin".lastChar()
// alias도 지정 가능하다.
import string.lastChar as last
val c = "Kotlin".last()
💡한 파일 안에서 다른 여러 개의 패키지에 속해 있는 이름이 같은 함수를 가져와 써야하는 경우 이름을 alias를 통해 바꿔서 임포트하면 이름 충돌을 막을 수 있다. 이것이 확장 함수의 이름 충돌을 해결할 수 있는 유일한 방법이다.
2. Java에서 확장 함수 호출
내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드다. 그래서 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점에 부가 비용이 들지 않는다.
다음은 StringUtil.kt
에 확장 함수를 정의한 것을 자바 코드로 변환한 것이다. 인자로 수신 객체만 넘기면 된다.
char c = StringUtilKt.lastChar("Java")
3. 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
suffix: String = ""
): String {
val result = StringBuilder(prefix)
for ((index, elem) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(elem)
}
result.append(suffix)
return result.toString()
}
val list = listOf(1, 2, 3)
println(list.joinToString(
separator = "; ", prefix = "(", suffix = ")")
) // (1; 2; 3)
💡확장 함수는 정적 메소드 호출에 대한 문법적 설탕(syntactic sugar)일 뿐이다. 따라서 클래스가 아닌 구체적인 타입을 수신 객체 타입으로 지정할 수도 있다. 위 예제의 제네릭 타입을 String으로 변경하면 String 타입의 Collection만 사용할 수 있다.
4. 확장 함수는 오버라이드할 수 없다.
확장 함수는 정적 바인딩이기 때문에 오버라이드할 수 없다.
open class View {
open fun click() = println("View clicked")
}
class Button: View() {
override fun click() = println("Button clicked")
}
val view: View = Button()
view.click() // Button clicked
Kotlin은 Java와 마찬가지로 오버라이드된 메소드 호출에 대해 동적 바인딩이 적용된다. 즉 런타임에 실행될 메소드가 결정된다. 하지만 확장 함수는 이렇게 동작하지 않는다. 확장 함수는 정적 바인딩이 적용되기 때문이다.
fun View.showOff() = println("I'm view")
fun Button.showOff() = println("I'm button")
val view: View = Button()
view.showOff() // I'm view
💡기존 클래스의 메소드와 확장 함수의 시그니처가 같다면 항상 멤버 메소드가 실행된다.
5. 확장 프로퍼티
확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다. 프로퍼티라고 불리기는 하지만 상태를 저장할 방법이 없기 때문에 확장 프로퍼티는 실제로 아무런 값도 가질 수 없다. 대신 코드의 간결성을 향상시킬 수 있다.
val String.lastChar: Char
get() = get(length - 1)
단순히 일반 프로퍼티에 수신 객체 클래스가 추가된 것이다. 뒷받침하는 필드가 없어서 기본 게터는 항상 정의해줘야 한다. 그리고 초기화 코드에서 계산한 값을 담을 수 없으므로 초기화 코드도 쓸 수 없다.StringBuilder
에 같은 프로퍼티를 정의하면 StringBuilder
의 맨 마지막 문자는 변경 가능하므로 프로퍼티를 var
로 만들 수 있다.
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)
Java에서 확장 프로퍼티를 사용하려면 항상 StringUtilKt.getLastChar("Java")
와 같이 getter를 호출해야 한다.
'Programming Languages > Kotlin' 카테고리의 다른 글
[Kotlin] 9. 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언 (0) | 2023.03.26 |
---|---|
[Kotlin] 8. 클래스 계층 정의 (0) | 2023.03.26 |
[Kotlin] 6. 함수 정의와 호출 (0) | 2023.03.24 |
[Kotlin] 5. 예외처리 (0) | 2023.03.23 |
[Kotlin] 4. 대상을 이터레이션: while 과 for (0) | 2023.03.23 |