본문 바로가기
코틀린/코틀린 공식문서

[코틀린 공식문서] Extensions; 확장

by 측면삼각근 2023. 7. 27.
728x90
반응형

해당 블로깅은 코틀린 공식문서의 Classes and objects의 Extensions를 번역하며 학습한 내용입니다.

학습중임에 따라 추가되고 의역된 부분이 있습니다.
혹시 잘못된 설명이 있다면 얼마든지 제보해주세요.

 

Extensions | Kotlin

 

kotlinlang.org


Extensions

코틀린은 데코레이터와 같은 디자인 패턴을 사용하지 않고, extension을 통해 클래스나 인터페이스에 새로운 기능을 추가할 수 있다.
예를들어, 수정할 수 없는 서드파티 라이브러리의 클래스나 인터페이스에 대해 새로운 함수를 작성 할 수 있다. 이러한 함수는 원래 클래스의 매소드인것 처럼 일반적인 방식으로 호출 할 수 있다. 
이러한 매커니즘을 extension function이라고 한다. 또한, 클래스에 대한 새로운 속성을 정의 할 수 있는 extension properties도 있다.

Extensions Functions

확장 함수를 선언하려면, 확장할 타입을 나타내는 수신자 타입으로 접두어를 붙인다.

다음은 MutableList<Int>에 swap함수를 추가하는 예시이다.

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

확장 함수 내부의 this는 reciever object(. 이전의 객체)에 해당한다.
이제 MutableList<Int>에서 위에서 정의한 swap함수가 호출이 가능해진다.

val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'this' inside 'swap()' will hold the value of 'list'

아래와 같이 모든 제네릭에서 사용이 가능하도록 할 수 도 있다.

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
}

Extensions are resoleved statically

Extensions기능은 실제 클래스를 수정하지는 않는다.
확장을 정의함으로써 클래스에 새 맴버를 삽입하는 것이 아니라, 이 유형의 변수에 점 표기법으로 호출 할 수 있는 새 함수를 만드는 것이다.

확장 함수는 정적으로 디스패치된다.
호출되는 확장 함수는 수신자 유형을 기반으로 컴파일 시점에 결정된다.

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클래스에만 의존하는 확장함수이기 때문에 Shape을 출력한다.

하지만 만약 클래스에 동일한 이름의 메서드가 있고, 동일한 수신자 타입, 인수를 갖는 확장함수가 정의된 경우 먼저 선언된 메서드가 항상 우선시된다.

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

fun Example.printFunctionType() { println("Extension function") }

Example().printFunctionType() // Class method

하지만, 동일한 이름의 다른 시그니처를 가진 메서드를 오버로드하는 것은 확장 함수에서  허용된다.

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

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

Example().printFunctionType(1) // Extension function #1

Nullalble receiver

확장 함수는 nullable 수신자 타입으로 정의될 수 있다.

이러한 확장함수는 값이 null인 객체 변수에서도 호출될 수 있다.
수신자가 null인 경우 `this`또한 null이다. 따라서 nullable 수신자 타입으로 extension을 정의할 때 컴파일 오류를 방지하기 위해 this == null 체크를 수행하는 것이 좋다.

코틀린에서는 이미 null체크가 확장 함수 내부에서 수행되므로, toString()을 null체크 없이 호출 할 수 있다.

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

Extension properties

코틀린은 확장 프로퍼티도 지원한다.

val <T> List<T>.lastIndex: Int
    get() = size - 1
확장 프로퍼티는 실제 클래스에 삽입하지 않으므로, 확장 프로퍼티에 지원 필드(backing field)를 가질 수 있는 효과적인 방법은 없다. (확장 프로퍼티는 컴파일 시점에 정적으로 결정되기 때문에 런타임 시점에 동적으로 결정되는 프로퍼티를 확장할 수 없다.)

이것은 확장 속성에 대해 이니셜 라이저가 허용되지 않는 이유이다. 명시적으로 getter/setter를 제공함으로써만 정의 될 수 있다.
(아래 더보기에 자세히 추가)
더보기

확장 프로퍼티 제약사항 예시

// Example class
class Person(val name: String)

// Extension property
val Person.upperCaseName: String
    get() = name.toUpperCase()

fun main() {
    val person = Person("John")
    println(person.upperCaseName) // Output: JOHN
}

백킹 필드(backing field) 없음

클래스에서 선언된 일반 속성과 달리 확장 속성에는 연결된 지원 필드가 없다.
backing field는 속성의 실제 값을 저장하는 데 사용된다. 백킹 필드는 원래 클래스의 일부가 아니므로 값을 저장하는 효율적인 방법은 없다.

// Regular property in a class with backing field
class MyClass {
    var myProperty: Int = 0
}

// Extension property without a backing field (Not allowed)
val MyClass.myExtensionProperty: Int = 0
// This is not allowed, as it requires a backing field

 이니셜 라이저 없음

배킹 필드가 없기 때문에, 확장 속성에 대한 이니셜라이저 제공을 하지 않는다.
이니셜라이저는 속성의 초기값을 정하는데 사용되나 확장 속성에서는 값을 저장할 위치가 없으므로 이니셜 라이저를 제공하는것은 의미가 없다.

명시적 getter/setter를 사용하여 해당 동작을 정의할 수 있다.

확장 속성(extention properties)는 구문상 일반 속성처럼 보일수는 있지만, 본질적으로 동작을 추가하기 위한 매커니즘이다.
런타임에 동적으로 결정된 속성을 직접 확장할 수는 없다.
값이 데이터를 직접 저장하는 것이 아니라, 코드를 통해 결정되기 때문에, 속성보다는 추가 메서드 함수에 가깝다

Companion object extensions

클래스에 companion object가 정의되어 있는 경우 companion object에 대한 확장 함수와 속성을 정의할 수도 있다.
companion object의 일반 멤버와 마찬가지로 클래스 이름만 한정자로 사용하여 호출할 수 있다.

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

fun MyClass.Companion.printCompanion() { println("companion") }

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

Scope of extensions

대부분의 경우 가장 상위의, 패키지 바로 아래에 확장을 정의한다.

package org.example.declarations

fun List<String>.getLongestString() { /*...*/}

선언 패키지 외부에서 확장을 사용하려면 import를 사용

package org.example.usage

import org.example.declarations.getLongestString

fun main() {
    val list = listOf("red", "green", "blue")
    list.getLongestString()
}

Declaring extensions as members

한 클래스에 대한 확장을 다른 클래스 내에서 선언할 수 있다.

이러한 확장 내부에는 여러 implict receivers(암시적 수신자; 한정자 없이 멤버에 액세스할 수 있는 개체)가 있다.
확장이 선언된 클래스의 인스턴스를 dispatch reciever라고 하고, 확장 메서드의 reciver type의 인스턴스를 extension reciever 라고 한다.

 

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()   // calls Host.printHostname()
        print(":")
        printPort()   // calls Connection.printPort()
    }

    fun connect() {
        /*...*/
        host.printConnectionString()   // calls the extension function
    }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    // kotl.in:443 kotl.in:443 
    //Host("kotl.in").printConnectionString()  // error, the extension function is unavailable outside Connection
}
1. extension reciever: Host는 Host.printConnectionString()의 extension reciever
2. dispatch reciever: Connection은 확장기능 Host.printConnectionString()의 dispatch reciever

즉, 확장 함수는 Connection 클래스 내에서 정의되며, Connection 인스턴스 내에서 호출 할 수 있다.

 

extension reciever와 dispaterch reciever의 멤버 간 이름이 충돌하는 경우 extension reciever이 우선시 된다.
dispatch reciever를 참조하려면 `this` 구문을 사용할 수 있다.

 

class Connection {
    fun Host.getConnectionString() {
        toString()         // calls Host.toString()
        this@Connection.toString()  // calls Connection.toString()
    }
}

맴버로 선언된 extension은 서브클래스로 선언되고, 서브클래스에서 재정의 될 수 있다.
이러한 함수는 dispatch reciever과 관련하여 가상이지만, 확장 수신자 유형과 관련하여 정적이다.

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()   // call the extension function
    }
}

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" - dispatch receiver is resolved virtually
    DerivedCaller().call(Derived())  // "Base extension function in DerivedCaller" - extension receiver is resolved statically
}

Note on visibility

extensions은 동일한 범위에서 선언된 일반 함수와 visibility modifiers (private, protected, internal, public..)을 사용한다.

  • 파일 최상위 수준(클래스 혹은 개체 외부)에서 확장 함수 또는 속성을 선언하면, 동일한 파일의 최상위 선언에 접근 할 수 있다.
    (파일에 선언된 확장이 해당 파일의 private function혹은 그 외 최상위 구성들에 접근 할 수 있음을 의미)
  • reciever유형(확장하는 클래스) 외부에서 extensions을 선언하면 해당하는 클래스의 private 혹은 protected 맴버에 접근 할 수 없다.
// Top-level private function
private fun privateTopLevelFunction() {
    println("This is a private top-level function.")
}

// Receiver class
class MyClass {
    private val privateProperty = "Private Property"

    fun callExtension() {
        // Extension function calling privateTopLevelFunction (accessible)
        this.printMessageFromTopLevel()
    }
}

// Extension function defined outside its receiver type
fun MyClass.printMessageFromTopLevel() {
    // Accessing the privateProperty (accessible)
    println("Message from extension: ${this.privateProperty}")

    // Accessing privateTopLevelFunction (accessible)
    privateTopLevelFunction()
}

fun main() {
    val myClass = MyClass()
    myClass.callExtension()
}

 

 

 

반응형