본문 바로가기

Kotlin

Kotlin - 확장(Extensions)

https://kotlinlang.org/docs/extensions.html

 

Extensions | Kotlin

 

kotlinlang.org

Kotlin은 확장이라는 특별한 정의 방법을 이용해서 클래스나 인터페이스의 기능을 상속 없이 확장 할 수 있다. 

예로, 직접 수정할 수 없는 서드파티 라이브러리 클래스나 인터페이스에 원하는 함수를 추가 할 수 있다. 이렇게 추가한 함수들은 일반 함수들과 똑같은 방법으로 호출하여 사용가능하다. 이런 동작방식을 확장 함수(extension function) 이라고 부르며, 같은 방식의 확장 속성 (extension properties)도 있다.

확장 함수 (Extension functions)

확장 함수를 정의하기 위해서는 함수명 앞에 해당 함수를 받을 타입을 작성 해 준다. 아래 예제는 MutableList<Int>에 swap 이라는 함수를 확장한 예 이다.

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1]
    this[index1] = this[index2]
    this[index2] = tmp
}

확장 함수에서 this 키워드는 확장 함수를 받는 오브젝트(receiver object)를 의미한다. 위와 같이 정의한 확장 함수는 아래 와 같이 일반함수와 똑같이 사용 할 수 있다.

fun usecaseOfSwap() {
    val list = mutableListOf(1, 2, 3)
    println("before swap: $list")
//    before swap: [1, 2, 3]
    list.swap(0, 2)
    println("after swap: $list")
//    after swap: [3, 2, 1]
}

 

Extensions are resolved statically

확장은 실제로 해당 클래스를 수정하는것이 아니다. 확장 함수는 정적으로 해당 클래스에 결합된다.

혹시 멤버 함수와 똑같은 확장 함수를 선언 한다면, 언제나 멤버 함수가 우선한다. (함수 오버로드는 예외)

class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType() { println("Extension function") }
fun Example.printFunctionType(i: Int) { println("Extension function overload : value is $i") }

fun main() {
    val ex = Example()
    ex.printFunctionType()
//    Class method
    ex.printFunctionType(2)
//    Extension function overload : value is 2
}

 

Nullable receiver

nullable receiver type 도 확장 함수를 선언 할 수 있다. 이 경우 this(receiver type) 은 null 일 수 있기 때문에 함수 내부에서 null 체크를 해야 한다.

fun Any?.toString(): String {
    if (this == null) return "null"
//	null 체크 이후 this 는 자동으로 non-nullable 타입으로 변경된다. 
    return toString()
}

 

Extension properties

확장 함수와 마찬가지로 확장 속성도 정의 할 수 있다.

확장은 클래스에 실제로 등록되는 것이 아니기 때문에 확장 속성의 경우 backing field 를  가질 수 없다. 그리고 이로 인해 확장 속성은 초기화가 허용되지 않고 명시적인 getter/setter를 통해서만 접근 가능하다.

// 오류 : initializers are not allowed for extension properties
//val <T> List<T>.lastIndex: Int = 10

val <T> List<T>.lastIndex: Int
    get() = size - 1

 

Companion object extensions

companion object(java 의 static 멤버)에도 확장 함수나 속성을 정의 할 수 있다. 

class MyClass {
    companion object { }  // will be called "Companion"
}

// Companion 키워드를 이용한다.
fun MyClass.Companion.printCompanion() { println("companion") }

fun main() {
    MyClass.printCompanion()
//    companion
}

 

Scope of extensions

대부분의 경우 패키지 바로 밑 최상위에 정의 한다. 그리고 이렇게 정의된 확장을 사용하기 위해서는 사용하고자 하는 곳에서 import 해서 사용한다.

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()
}

 

Declaring extensions as members

클래스 내부에서 다른 클래스의 확장을 선언 할 수 있다. 이런 확장에서는 암시적으로 여러개의 receiver가 존재 할 수 있다. 이 경우 확장이 선언된 클래스 를 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) }

    // Host 클래스가 extension receiver
    // Connection 클래스가 dispatch receiver
    fun Host.printConnectionString() {
        printHostname()
        print(":")
        printPort()
    }

    fun connect() {
        host.printConnectionString()
    }
}

fun main() {
    val host = Host("kotl.in")
    Connection(host, 443).connect()
//    host.printConnectionString() // 컴파일 에러, 확장 함수는 Connection 클래스 안에서만 사용 가능
}

만약 dispatch receiver 와 extension receiver 에 동일한 멤버의 이름이 존재 한다면 "this@클래스명" 문법을 이용해서 정확히 어느 클래스의 멤버인지 명시 해 줘야 한다.

fun Host.getConnectionString() {
    toString() // Host의 toString()을 호출
    this@Connection.toString() // Connection의 toString()을 호출
}

확장 멤버는 open 키워드를 이용해서 서브 클래스에서 재 정의 할 수 있도록 선언 할 수 있다. 이는 dispatch receiver에서는 가상화 되어 있지만 extension receiver 타입에서는 정적으로 동작함을 의미한다. (무슨말인지...)
Extensions declared as members can be declared as open and overridden in subclasses. This means that the dispatch of such functions is virtual with regard to the dispatch receiver type, but static with regard to the extension receiver type.

class Host(val hostname: String) {
    fun printHostname() { print(hostname) }
}

class Connection(val host: Host, val port: Int) {
    fun printPort() { print(port) }

    // Host 클래스가 extension receiver
    // Connection 클래스가 dispatch receiver
    fun Host.printConnectionString() {
        printHostname()
        print(":")
        printPort()
    }

    fun Host.getConnectionString() {
        toString() // Host의 toString()을 호출
        this@Connection.toString() // Connection의 toString()을 호출
    }

    fun connect() {
        host.printConnectionString()
    }
}

open class ExtensionBase {}
class ExtensionDerived : ExtensionBase() {}

open class ExtensionBaseCaller {
    open fun ExtensionBase.printFunction() {
        println("Base extension function in ExtensionBaseCaller")
    }

    open fun ExtensionDerived.printFunction() {
        println("Derived extension function in ExtensionBaseCaller")
    }

    fun call(b: ExtensionBase) {
        b.printFunction()
    }

    fun callDerived(b: ExtensionDerived) {
        b.printFunction()
    }
}

class ExtensionDerivedCaller : ExtensionBaseCaller() {
    override fun ExtensionBase.printFunction() {
        println("Base extension function in ExtensionDerivedCaller")
    }

    override fun ExtensionDerived.printFunction() {
        println("Derived extension function in ExtensionDerivedCaller")
    }
}

fun main() {
    ExtensionBaseCaller().call(ExtensionBase())
//    Base extension function in ExtensionBaseCaller
    ExtensionBaseCaller().call(ExtensionDerived())
//    Base extension function in ExtensionBaseCaller

    ExtensionBaseCaller().callDerived(ExtensionDerived())
//    Derived extension function in ExtensionBaseCaller

    ExtensionDerivedCaller().call(ExtensionBase())
//    Base extension function in ExtensionDerivedCaller - dispatch receiver is resolved virtually
    ExtensionDerivedCaller().call(ExtensionDerived())
//    Base extension function in ExtensionDerivedCaller - extension receiver is resolved statically

    ExtensionDerivedCaller().callDerived(ExtensionDerived())
//    Derived extension function in ExtensionDerivedCaller
}

 

Note on visibility

확장은 일반 함수와 동일한 액세스 한정자를 갖는다.

  • 파일의 최상위 수준에 선언된 확장은 동일 파일의 최상위 수준에 선언된  private에 액세스할 수 있다.
  • 확장은  receiver type의 private또는 protected멤버에 액세스할 수 없다.
반응형