(Kotlin) 확장 함수 / 확장 프로퍼티, 최상위 함수 / 최상위 프로퍼티
확장 함수 / 확장 프로퍼티
- Note ) 코틀린의 최상위 객체는
Any
이기 때문에, 여기에 추가하면 모든 객체에 확장 함수를 추가할 수 있다. - 확장 함수를 사용하면 좋은 경우는
- String 같은 기본 타입을 확장하고 싶을 때. e.g., String.appendSignature (public으로 쓰는 경우도 많고, 시그니처를 사용하는 부분이 제한되어 있다면 private으로 만들면 아주 깔끔)
- 내가 만든 클래스라 마음대로 메서드 정의가 가능한 상황일지라도, 클래스에 포함하자니 무언가 애매한 기능일 때. (역시 보통은 사용처가 제한되는 경우가 많아서 private)
- 확장 함수를 사용해도 실행 시점에 부가 비용이 들지 않는다.
- 확장 함수가 클래스를 노출시키면 안되므로, 확장 함수의 가시성은 클래스의 가시성과 같거나 더 낮아야 한다.
확장 함수 ( extension function )
1
2
3
4
5
6
package strings
fun String.lastChar(): Char = this.get(this.length - 1) // this 생략 가능
private fun String.appendSignature() =
"$this&signature=${HmacUtils(HmacAlgorithms.HMAC\_SHA\_256, secretKey).hmacHex(this)}"
사용하려면 import
해야한다.
1
2
3
import strings.lastChar
println("Kotlin".lastChar())
클래스 이름(String
)을 수신 객체 타입(receiver type)이라고 부르며, 수신 객체(receiver object)는 확장 함수에 접근할 때 사용하게 되는 객체를 의미한다. 확장 함수가 캡슐화를 깰 수는 없는게, 확장 함수는 내부에서만 사용할 수 있는 private, protected
멤버에 접근할 수 없다. 확장 함수 보다 멤버 함수의 우선 순위가 더 높기 때문에 같은 이름을 가졌다면 멤버 함수가 호출된다.
확장 함수는 하위 클래스에서 오버라이드할 수 없다.
확장 함수는 클래스의 일부가 아니라, 클래스 밖에 선언되며 상속되지 않는다. 어떤 확장 함수를 호출할지는 수신 객체로 지정한 변수의 정적 타입에 의해 결정된다. ( 정적 디스패치, static dispatch ) 확장 함수가 아닌 일반적인 메서드의 경우 상속 계층에서 오버라이드한 함수를 호출할 때 변수의 동적 타입에 의해 호출 함수가 결정되기 때문에 오버라이드한 함수가 호출된다. ( 동적 디스패치, dynamic dispatch )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
open class View {
open fun click() = println("View normal method")
}
class Button: View() {
override fun click() = println("Button normal method")
}
fun View.extFunc() = println("View extFunc")
fun Button.extFunc() = println("Button extFunc")
>>> val view: View = Button() // View type 변수에 Button() 객체 할당
>>> view.click()
Button normal method // dynamic
>>> view.extFunc()
View extFunc // static
Note ) 내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드로 변환된다.
그래서 자바에서 확장 함수를 사용할 때는 이런 식으로 사용하게 되는데, 이를 생각해보면 왜 상속되지 않는지 이해가 된다.
1
2
ExtensionsKt.extFunc(view)
확장 프로퍼티 ( extension property )
확장 함수와 비슷하게 기존 클래스 객체에 프로퍼티도 추가할 수 있는데, 프로퍼티라는 이름으로 불리기는 하지만 기존 클래스의 인스턴스 객체에 필드를 추가할 방법이 없기 때문에(상태를 저장할 방법이 없기 때문에) 실제로 확장 프로퍼티는 아무 상태(필드)도 가질 수가 없다. 그때그때 evaluation해서 얻어내는 값이기 때문에 게터는 함수와 쓰임에 큰 차이가 없으나 세터를 다음과 같이 정의하면 여러모로 이점이 있다.
1
2
3
4
5
6
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
필드를 갖지 않으므로 기본 게터 구현을 제공할 수 없어 최소한 게터는 반드시 정의해야 한다.
최상위 수준 함수 / 프로퍼티 (Top-Level Functions)
코틀린은 자바와 달리 최상위 수준에 함수나 프로퍼티를 정의할 수 있다. JVM은 클래스 안에 들어있는 코드만 실행할 수 있기 때문에 최상위 수준 함수는 자동으로 파일 명을 클래스 명으로 하는 클래스에 들어가게 된다.
1
2
3
4
5
6
// filename : join.kt를 java로 변환하면
package strings;
public class JoinKt {
public static string joinTostring(...) { ... }
}
최상위 함수가 포함되는 클래스 이름을 변경하고 싶다면
1
2
@file:JvmNanem("StringFunctions")
정적 유틸리티 클래스 vs 최상위 함수
- 정적 클래스는 한 클래스에 여러 유틸리티 메소드를 묶어 네임스페이스를 관리 할 수 있다는 장점이 있다. (kotlin에서는 companion object)
- 최상위 함수는 함수들에 대한 관리는 File 단위로 한다지만, 각각의 함수들이 global namespace를 사용하기 때문에 충돌을 조금 더 신경써야 한다.
- 실제로 사용 할 때는 class prefix 없이 함수만 명시해도 되는 최상위 함수가 더 깔끔한 경우가 많다.