•
Kotlin은 클래스에서 상속하거나 Decorator 와 같은 디자인 패턴을 사용하지 않고도 클래스나 인터페이스를 새로운 기능으로 확장할 수 있는 기능을 제공한다.
•
이는 확장이라고 하는 특수 선언을 통해 수행된다.
•
수정할 수 없는 타사 라이브러리의 클래스나 인터페이스에 대한 새 함수를 작성할 수 있다.
•
이 메커니즘을 확장 함수라고 하고 기존 클래스에 대한 새 속성을 정의할 수 있는 확장 속성도 존재한다.
확장 함수
•
확장 함수를 선언하려면 이름 앞에 확장되는 타입을 나타내는 수신자 타입을 접두사로 붙인다.
•
확장 함수 내부의 키워드 this는 수신자 객체에 해당한다.
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' corresponds to the list
this[index1] = this[index2]
this[index2] = tmp
}
Kotlin
복사
•
제네릭 타입 매개변수를 통해 일반화할 수 있다.
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
}
Kotlin
복사
확장 함수는 정적으로 결정된다.
•
확장은 실제로 확장하는 클래스를 수정하지 않는다.
•
확장을 정의하면 클래스에 새 멤버를 삽입하지 않고 이 타입의 변수에 점 표기법으로 호출 가능한 새 함수를 만든다.
•
확장 함수는 정적으로 전송되므로 어떤 확장 함수가 호출되는지는 수신기 타입에 따라 컴파일 시간에 알려진다.
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(s: Shape) {
println(s.getName())
}
printClassName(Rectangle())
Kotlin
복사
•
printClassName의 파라미터 f가 Shape기 때문에 Shape의 확장 함수인 getName을 호출한다.
확장 함수의 우선순위
•
클래스에 멤버 함수와 동일한 수신기 타입, 동일한 이름, 동일한 인수를 가지는 확장 함수가 정의된 경우 항상 멤버 함수가 호출된다.
class Example {
fun printFunctionType() { println("Class method") }
}
fun Example.printFunctionType() { println("Extension function") }
Example().printFunctionType()
// Class method
Kotlin
복사
오버로딩
•
확장 함수는 동일한 이름이지만 다른 시그니처를 가진 경우 멤버 함수를 오버로드하는 것이 가능하다.
class Example {
fun printFunctionType() { println("Class method") }
}
fun Example.printFunctionType(i: Int) { println("Extension function #$i") }
Example().printFunctionType(1)
// Extension function #1
Kotlin
복사
Nullable 수신기
•
확장은 nullable 수신기 타입으로 정의할 수 있고 값이 null이더라도 객체 변수에서 호출할 수 있다.
•
수신기가 null이면 this도 null이다.
•
nullable 수신기 타입으로 확장할 때는 컴파일러 오류를 방지하기 위해 함수 본문 내부에서 this == null 검사를 수행하는 것이 좋다
fun Any?.toString(): String {
if (this == null) return "null"
// After the null check, 'this' is autocast to a non-nullable type, so the toString() below
// resolves to the member function of the Any class
return toString()
}
Kotlin
복사
확장 속성
•
확장 속성은 클래스에 실제 멤버를 추가하는 것이 아니기 때문에 백킹 필드를 가질 수 없다.
•
확장 속성은 초기화자가 허용되지 않으므로 초기값을 지정할 수 없다.
•
확장 속성은 반드시 getter와 setter를 명시적으로 제공해야 한다.
val <T> List<T>.lastIndex: Int
get() = size - 1
Kotlin
복사
동반 객체 확장
•
클래스에 companion object가 정의되어 있는 경우 동반 객체에 대해 확장 함수와 확장 속성을 정의할 수 있다.
•
동반 객체의 기본 이름은 Companion으로 동반 객체 확장은 클래스 이름을 사용하여 호출할 수 있다.
class MyClass {
companion object { } // will be called "Companion"
}
// 동반 객체 확장 함수
fun MyClass.Companion.printCompanion() { println("companion") }
// 동반 객체 확장 속성
val MyClass.Companion.description: String
get() = "This is a companion object."
fun main() {
MyClass.printCompanion() // 출력: companion
println(MyClass.description) // 출력: This is a companion object.
}
Kotlin
복사
확장 범위
•
확장 함수와 확장 속성은 일반적으로 확장이 정의된 패키지 내에서만 유효하다는 것을 의미하기 위해 패키지의 최상위 레벨에 정의한다.
package org.example.declarations
fun List<String>.getLongestString(): String {
return this.maxByOrNull { it.length } ?: ""
}
Kotlin
복사
•
이 확장 함수는 org.example.declarations 패키지 내에서만 사용 가능하다.
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
val longest = list.getLongestString()
println(longest) // 출력: green
}
Kotlin
복사
•
다른 패키지에서 사용하려면 해당 확장 함수를 import 해야 한다.
확장을 멤버로 선언
•
클래스 내에 다른 클래스에 대한 확장 함수를 선언할 수 있다.
•
이러한 확장 함수 안엔 별도의 수식어 없이 접근할 수 있는 여러 암묵적 수신기가 존재할 수 있다.
•
이러한 확장 함수가 선언된 클래스의 인스턴스를 디스패치 수신기라고 한다.
•
확장 함수의 수신기 타입의 인스턴스를 확장 수신기라고 한다.
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 클래스 밖에서 사용 불가
}
Kotlin
복사
•
Connection 클래스에서 Host 클래스에 대한 확장 함수 printConnectionString() 을 정의한다.
•
Host.printConnectionString() 의 암묵적 수신기는 Host와 Connection이다.
•
Connection은 디스패치 수신기이다.
•
Host는 확장 수신기이다.
이름 충돌
•
디스패치 수신기와 확장 수신기의 멤버 이름이 충돌하는 경우 확장 수신기의 멤버가 우선된다.
•
디스패치 수신기의 멤버를 참조하려면 this 키워드를 사용해야 한다.
class Connection {
fun Host.getConnectionString() {
toString() // Host.toString() 호출
this@Connection.toString() // Connection.toString() 호출
}
}
Kotlin
복사
확장 함수의 오버라이드
•
멤버로 선언된 확장 함수는 open 으로 선언되어 자식 클래스에서 오버라이드 될 수 있다.
•
디스패치 수신기 타입에 대해 가상으로 디스패치되지만 확장 수신기 타입에 대해서는 정적으로 디스패치된다는 것을 의미한다.
open class Base { }
class Derived : Base() { }
open class BaseCaller {
open fun Base.printFunctionInfo() {
println("1")
}
open fun Derived.printFunctionInfo() {
println("2")
}
fun call(b: Base) {
b.printFunctionInfo() // 확장 함수 호출
}
}
class DerivedCaller: BaseCaller() {
override fun Base.printFunctionInfo() {
println("3")
}
override fun Derived.printFunctionInfo() {
println("4")
}
}
fun main() {
BaseCaller().call(Base()) // "1"
BaseCaller().call(Derived()) // "1"
DerivedCaller().call(Base()) // "3"
DerivedCaller().call(Derived()) // "3"
}
Kotlin
복사
•
확장 함수는 기본적으로 정적 디스패치이기 때문에 컴파일 시점에 타입에 따라 어떤 함수가 호출될지 결정된다.
◦
call()의 인자로 Base를 받고 있기 때문에 Base의 자식 클래스인 Derived를 넘겨도 Base의 확장 함수가 호출된다.
•
클래스 멤버로 확장 함수를 선언하면 가상 디스패치가 가능하기 때문에 디스패치 수신기 타입에 따라 어떤 함수가 호출될지 결정된다.
◦
BaseCaller, DerivedCaller 중 어떤 디스패치 수신기냐에 따라서 호출되는 확장 함수가 달라진다.
확장의 가시성
•
확장은 동일한 범위에서 선언된 일반 함수와 동일한 가시성 수정자를 사용한다.
◦
파일의 최상위에 선언된 확장은 동일 파일 내의 다른 private 최상위 선언에 접근할 수 있다.
// File: Example.kt
private fun topLevelFunction() {
println("I am a private top-level function")
}
fun String.printWithTopLevelFunction() {
topLevelFunction() // 접근 가능
println(this)
}
fun main() {
"Hello".printWithTopLevelFunction() // 출력: I am a private top-level function
// Hello
}
Kotlin
복사
◦
수신기 타입 밖에 선언된 확장이라면 수신기의 private 또는 protected 멤버들에 접근할 수 없다.
class ExampleClass {
private val secret = "This is a secret"
fun printSecret() {
println(secret) // 접근 가능
}
}
fun ExampleClass.showSecret() {
// println(secret) // 접근 불가 - 컴파일 오류 발생
println("Cannot access private member")
}
fun main() {
val example = ExampleClass()
example.printSecret() // 출력: This is a secret
example.showSecret() // 출력: Cannot access private member
}
Kotlin
복사