확장
클래스를 상속하지 않고, 혹은 Decorator 등의 임의의 타입의 디자인 패턴을 사용하지 않고 클래스에 새로운 기능을 확장
확장함수
확장함수를 선언하기 위해서는 함수 이름 앞에 확장될 타입을 추가할 필요가 있다.
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' 는 리스트 객체
this[index1] = this[index2]
this[index2] = tmp
}
swap 기능을 추가한 MutableList<Int>
확장함수내의 this 키워드는 리시버 객체 (점 기호 앞에 넘겨진 객체)
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' corresponds to the list
this[index1] = this[index2]
this[index2] = tmp
}
이 함수는
MutableList<T>
의 제네릭 형태로도 가능
제네릭 타입 파라매터를 선언하고서 리시버 타입으로 합수명을 사용할수 있다. 제네릭 함수 참고
정적인 확장 함수
확장기능은 실제로 확장하는 클래스를 변경하는 것은 아니다. 단순히 그 타입의 변수의 점 표기에 새로운 함수를 호출할수 있도록 하는 것.
실행할 확장 함수는 정적으로 결정한다. 확장 함수를 호출하는 코드의 타 입으로 호출할 확장 함수를 결정한다. 런타임에 그 식을 평가한 결과 타입으로 결정하지 않는다
open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
println(c.foo())
}
printFoo(D())
c 가 출력. 호출되는 확장함수 c는 C클래스에 있는 파라매터로 선언된 타입에만 의존하기 때문
클래스에 멤버 함수가 있고, 동일한 반환타입, 동일한 이름을 가지고, 주어진 인수에 적용가능한 확장함수가 정의되어 있는 경우 멤버가 항상 우선시된다.
class C {
fun foo() { println("member") }
}
fun C.foo() { println("extension") }
Nullable Receiver
확장자는 nullable한 타입으로 정의되어 있다는 것에 주의. 이러한 확장은 그 값이 null 인 경우에도 객체 변수에서 호출 가능하고 본문에서 this == null
으로 체크할 수 있다. 이것은 null을 체크하지 않고 Kotlin에서 toString()을 호출할 수 있는 이유.
fun Any?.toString(): String {
if (this == null) return "null"
// after the null check, 'this' is autocast to a non-null type, so the toString() below
// resolves to the member function of the Any class
return toString()
}
확장 프로퍼티
함수와 동일하게 Kotlin은 확장 프로퍼티를 지원
val <T> List<T>.lastIndex: Int
get() = size - 1
확장기능은 실제로 멤버를 클래스에 주입하지않으므로, 확장 프로퍼티는 Backing Field를 가지지 않음. 이것이 확장 프로퍼티에 대해 initializer를 허용하지 않는 이유이다. 프로퍼티 동작은 getters/setter를 명시적으로 제공하는 것만 정의 가능하다.
val Foo.bar = 1 // 에러 : 확장 프로퍼티에 대한 initializer는 허용하지 않음
컴피니언 오브젝트 확장
클래스에 컴피니언 오브젝트가 정의되어있는 경우, 컴피니언 오브젝트의 확장기능과 프로퍼티를 정의가능
class MyClass {
companion object { } // "Companion" 으로 호출됨
}
fun MyClass.Companion.foo() {
// ...
}
컴피니언 오브젝트로 일반적인 멤버와 동일하게 클래스명으로 호출가능
MyClass.foo()
확장 범위
대부분 최상위 레벨인 패키지의 아래에 확장을 정의
package foo.bar
fun Baz.goo() { ... }
확장을 선언하고 있는 패키지 밖에서 사용할 경우 호출측에서 import 해야함
package com.example.usage
import foo.bar.goo // "goo"의 모든 확장을 임포트
// 혹은
import foo.bar.* // "foo.bar"에서 모든 것을 임포트
fun usage(baz: Baz) {
baz.goo()
)
import 참조
멤버로 확장기능 선언
클래스 안에서 다른 클래스의 확장을 선언할 수 있다. 확장 내에서는 복수의 암묵적인 리시버가 잇다. 객체의 멤버에는 제한자없이 접근가능하다. 확장이 선언된 클래스의 인스턴스는 디스패치 리시버라고 하며, 확장 메소드의 리시버 타입의 인스턴스는 확장 리시버라고 한다.
class D {
fun bar() { ... }
}
class C {
fun baz() { ... }
fun D.foo() {
bar() // D.bar 호출
baz() // C.baz 호출
}
fun caller(d: D) {
d.foo() // 확장 함수 호출
}
}
디스패치 리시버와 확장 리시버의 이름이 충돌하는 경우, 확장 리시버가 우선된다. 디스패치 리시버의 멤버를 참조할 경우에는 한정된 this 구문을 사용 가능하다
class C {
fun D.foo() {
toString() // D.toString() 호출
[email protected]() // C.toString() 호출
}
멤버로 선언된 open 확장은 서브 클래스로 선언되고 오버라이드된다. 이는 확장 함수를 디스패치 리시버 타입에 따라 선택한다는 것을 의미한다. 하지만 확장 리시버 타입은 정적이다.
open class D {
}
class D1 : D() {
}
open class C {
open fun D.foo() {
println("D.foo in C")
}
open fun D1.foo() {
println("D1.foo in C")
}
fun caller(d: D) {
d.foo() // 확장 함수 호출
}
}
class C1 : C() {
override fun D.foo() {
println("D.foo in C1")
}
override fun D1.foo() {
println("D1.foo in C1")
}
}
C().caller(D()) // "D.foo in C" 출력
C1().caller(D()) // "D.foo in C1" 출력 - 디스패치 리시버를 virtually하게 선택
C().caller(D1()) // "D.foo in C" 출력 - 확장 리시버를 정적으로 선택
동기
자바에서는 FileUtils
, StringUtils
처럼 “*Utils”라는 이름을 갖는 클래스에 익숙하다. 잘 알려진 java.util.Collections
도 이에 속한다. 이런 유틸리티 클래스가 싫은 이유는 코드가 다음과 같은 모습을 띄기 때문이다.
// Java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list))
정적 임포트를 사용하면 다음과 같이 된다
// Java
swap(list, binarySearch(list, max(otherList)), max(list))
다음 코드처럼 할 수 있다면 훨씬 나을 것이다.
// Java
list.swap(list.binarySearch(otherList.max()), list.max())
하지만 List
클래스에 가능한 메소드를 전부 구현하고 싶지않는다, 그렇죠? 이것이 확장이 우리를 도움이 되는 곳입니다.