Search

Extensions

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
복사