확장
코틀린은 클래스에서 상속하거나 _데코레이터_와 같은 디자인 패턴을 사용하지 않고도 클래스나 인터페이스에 새로운 기능을 확장하는 기능을 제공합니다. 이는 _확장(extensions)_이라고 불리는 특별한 선언을 통해 이루어집니다.
예를 들어, 수정할 수 없는 서드파티 라이브러리의 클래스나 인터페이스에 새로운 함수를 작성할 수 있습니다. 이러한 함수는 마치 원래 클래스의 메서드인 것처럼 일반적인 방식으로 호출할 수 있습니다. 이 메커니즘을 _확장 함수(extension function)_라고 합니다. 기존 클래스에 새로운 프로퍼티를 정의할 수 있게 해주는 _확장 프로퍼티(extension properties)_도 있습니다.
확장 함수
확장 함수를 선언하려면 함수 이름 앞에 확장하려는 타입을 나타내는 _리시버 타입(receiver type)_을 붙입니다. 다음은 MutableList<Int>
에 swap
함수를 추가하는 예시입니다.
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this'는 리스트에 해당합니다.
this[index1] = this[index2]
this[index2] = tmp
}
확장 함수 내부의 this
키워드는 리시버 객체(점 앞에 전달되는 객체)에 해당합니다. 이제 어떤 MutableList<Int>
에서도 이 함수를 호출할 수 있습니다.
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'swap()' 내부의 'this'는 'list'의 값을 가집니다.
이 함수는 어떤 MutableList<T>
에도 유용하며, 제네릭으로 만들 수 있습니다.
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this'는 리스트에 해당합니다.
this[index1] = this[index2]
this[index2] = tmp
}
제네릭 타입 파라미터를 리시버 타입 표현식에서 사용할 수 있게 하려면 함수 이름 앞에 선언해야 합니다. 제네릭에 대한 자세한 내용은 제네릭 함수를 참조하세요.
확장은 정적으로 해결됩니다.
확장은 실제로 확장하는 클래스를 수정하지 않습니다. 확장을 정의함으로써 클래스에 새 멤버를 삽입하는 것이 아니라, 해당 타입의 변수에 대해 점 표기법으로 새 함수를 호출할 수 있도록 만드는 것뿐입니다.
확장 함수는 정적으로 디스패치됩니다. 따라서 어떤 확장 함수가 호출될지는 컴파일 시간에 리시버 타입을 기반으로 이미 결정됩니다. 예를 들어:
fun main() {
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(s: Shape) {
println(s.getName())
}
printClassName(Rectangle())
}
이 예시는 _Shape_를 출력합니다. 왜냐하면 호출되는 확장 함수는 파라미터 s
의 선언된 타입(이는 Shape
클래스임)에만 의존하기 때문입니다.
클래스에 멤버 함수가 있고, 동일한 리시버 타입과 동일한 이름을 가지며 주어진 인수에 적용 가능한 확장 함수가 정의되어 있다면, 멤버가 항상 우선합니다. 예를 들어:
fun main() {
class Example {
fun printFunctionType() { println("Class method") }
}
fun Example.printFunctionType() { println("Extension function") }
Example().printFunctionType()
//endSample
}
이 코드는 _Class method_를 출력합니다.
하지만 확장 함수가 동일한 이름이지만 다른 시그니처를 가진 멤버 함수를 오버로드하는 것은 전혀 문제가 없습니다.
fun main() {
class Example {
fun printFunctionType() { println("Class method") }
}
fun Example.printFunctionType(i: Int) { println("Extension function #$i") }
Example().printFunctionType(1)
//endSample
}
널러블 리시버
확장은 널러블 리시버 타입으로 정의될 수 있습니다. 이러한 확장은 객체 변수의 값이 null
이더라도 호출될 수 있습니다. 리시버가 null
이면 this
도 null
입니다. 따라서 널러블 리시버 타입으로 확장을 정의할 때, 컴파일러 오류를 피하기 위해 함수 본문 내에서 this == null
검사를 수행하는 것이 좋습니다.
코틀린에서는 null
검사 없이 toString()
을 호출할 수 있습니다. 검사가 확장 함수 내에서 이미 일어나기 때문입니다.
fun Any?.toString(): String {
if (this == null) return "null"
// 널 검사 후, 'this'는 널 불가능 타입으로 자동 캐스팅되므로, 아래의 toString()은
// Any 클래스의 멤버 함수로 해결됩니다.
return toString()
}
확장 프로퍼티
코틀린은 함수를 지원하는 것과 유사하게 확장 프로퍼티도 지원합니다.
val <T> List<T>.lastIndex: Int
get() = size - 1
확장은 실제로 클래스에 멤버를 삽입하지 않으므로, 확장 프로퍼티가 배킹 필드(backing field)를 갖는 효율적인 방법은 없습니다. 이것이 _확장 프로퍼티에 초기화를 허용하지 않는 이유_입니다. 이들의 동작은 명시적으로 게터/세터를 제공함으로써만 정의될 수 있습니다.
예시:
val House.number = 1 // 오류: 확장 프로퍼티에는 초기화를 허용하지 않습니다.
동반 객체 확장
클래스에 동반 객체(companion object)가 정의되어 있다면, 동반 객체에 대한 확장 함수 및 프로퍼티도 정의할 수 있습니다. 동반 객체의 일반 멤버와 마찬가지로, 클래스 이름만 한정자로 사용하여 호출할 수 있습니다.
class MyClass {
companion object { } // "Companion"이라고 불립니다.
}
fun MyClass.Companion.printCompanion() { println("companion") }
fun main() {
MyClass.printCompanion()
}
확장의 스코프
대부분의 경우 확장을 패키지 바로 아래 최상위 수준에서 정의합니다.
package org.example.declarations
fun List<String>.getLongestString() { /*...*/ }
확장을 선언된 패키지 외부에서 사용하려면 호출 지점에서 임포트해야 합니다.
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}
자세한 내용은 임포트를 참조하세요.
멤버로 확장 선언하기
다른 클래스 내부에 한 클래스에 대한 확장을 선언할 수 있습니다. 이러한 확장 내부에는 여러 _암시적 리시버(implicit receivers)_가 있습니다. 이들은 한정자 없이 멤버에 접근할 수 있는 객체입니다. 확장이 선언된 클래스의 인스턴스를 _디스패치 리시버(dispatch receiver)_라고 하며, 확장 메서드의 리시버 타입 인스턴스를 _확장 리시버(extension receiver)_라고 합니다.
class Host(val hostname: String) {
fun printHostname() { print(hostname) }
}
class Connection(val host: Host, val port: Int) {
fun printPort() { print(port) }
fun Host.printConnectionString() {
printHostname() // Host.printHostname()을 호출합니다.
print(":")
printPort() // Connection.printPort()를 호출합니다.
}
fun connect() {
/*...*/
host.printConnectionString() // 확장 함수를 호출합니다.
}
}
fun main() {
Connection(Host("kotl.in"), 443).connect()
//Host("kotl.in").printConnectionString() // 오류, 확장 함수는 Connection 외부에서 사용할 수 없습니다.
}
디스패치 리시버와 확장 리시버의 멤버 간에 이름 충돌이 발생하는 경우, 확장 리시버가 우선권을 가집니다. 디스패치 리시버의 멤버를 참조하려면 한정된 this
구문(qualified this
syntax)을 사용할 수 있습니다.
class Connection {
fun Host.getConnectionString() {
toString() // Host.toString()을 호출합니다.
this@Connection.toString() // Connection.toString()을 호출합니다.
}
}
멤버로 선언된 확장은 open
으로 선언될 수 있으며 서브클래스에서 오버라이드될 수 있습니다. 이는 이러한 함수의 디스패치가 디스패치 리시버 타입에 대해서는 가상으로 이루어지지만, 확장 리시버 타입에 대해서는 정적으로 이루어진다는 것을 의미합니다.
open class Base { }
class Derived : Base() { }
open class BaseCaller {
open fun Base.printFunctionInfo() {
println("Base extension function in BaseCaller")
}
open fun Derived.printFunctionInfo() {
println("Derived extension function in BaseCaller")
}
fun call(b: Base) {
b.printFunctionInfo() // 확장 함수를 호출합니다.
}
}
class DerivedCaller: BaseCaller() {
override fun Base.printFunctionInfo() {
println("Base extension function in DerivedCaller")
}
override fun Derived.printFunctionInfo() {
println("Derived extension function in DerivedCaller")
}
}
fun main() {
BaseCaller().call(Base()) // "Base extension function in BaseCaller"
DerivedCaller().call(Base()) // "Base extension function in DerivedCaller" - 디스패치 리시버는 가상으로 해결됩니다.
DerivedCaller().call(Derived()) // "Base extension function in DerivedCaller" - 확장 리시버는 정적으로 해결됩니다.
}
가시성에 대한 참고사항
확장은 동일한 스코프 내에 선언된 일반 함수와 동일한 가시성 한정자(visibility modifiers)를 사용합니다. 예를 들어:
- 파일의 최상위 수준에 선언된 확장은 동일한 파일 내의 다른
private
최상위 선언에 접근할 수 있습니다. - 확장이 리시버 타입 외부에서 선언된 경우, 리시버의
private
또는protected
멤버에 접근할 수 없습니다.