2024. 2. 1. 19:54ㆍ프로그래밍 언어/Kotlin
Kotlin 공식 document를 기반으로 작성됨.
https://kotlinlang.org/docs/extensions.html
Extensions | Kotlin
kotlinlang.org
이번 블로그에서는 Kotlin에서 지원하는 Extension에 대해서 소개해보려고 한다.
Android developer page에서 companion object에 대해서 공부를 하다가, extension에 관한 내용이 나왔는데, function이나 class에 따라서도 extension을 수행하는 방법이 다른거 같아서 kotlin의 extension을 전체적으로 정리해보고자 한다.
Extension
Kotlin Extension은 클래스의 상속이나 Decorate와 같은 디자인 패턴을 사용하지 않고도 새로운 기능으로 클래스나 인터페이스를 확장할 수 있는 기능을 뜻한다.
Decorate 디자인 패턴의 경우 Interface와 abstract class를 통해 클래스의 확장의 역할을 수행하도록 하는 디자인 패턴이다. 이는 나중에 디자인 패턴에 관해 포스팅 할 시간이 있다면 소개해보도록 하겠다.
Extension의 경우 외부 라이브러리의 클래스 또는 인터페이스에 대한 새로운 method 혹은 property를 작성할 수 있고, 마치 원래 클래스의 component 인 것처럼 일반적인 방법으로 호출할 수 있다. 확장이 행해지는 객체에 대해서 우리는 Receiver라고 부를 수 있다.
Extension을 수행하는 방법을 한번 알아보자.
MutableList 유형의 객체에 대해서 component 2개를 swap하는 method를 extension 해보도록 하자.
fun main() {
fun <T> MutableList<T>. swap(index1: Int, index2: Int) {
val temp = this[index1]
this[index1] = this[index2]
this[index2] = temp
}
val list = mutableListOf<Int>(1, 2, 3)
println(list)
// [1, 2, 3]
list.swap(0,2)
println(list)
// [3, 2, 1]
}
기존 우리가 Kotlin에서 사용하는 MutableList에 swap이라는 method를 extension한 것으로, method 본문의 this는 객체 자신을 가르키게 된다.
Receiver의 method를 호출하는 방법은 앞서 설명했듯이 간단히 일반적인 메서드를 호출하는 방법과 동일하다.
Extensions are resolved statically
부연 설명으로 공식 문서에서는 "Extensions are resolved statically" 라고 설명하고 있다.
겉으로만 봤을 때에는, Extension이 클래스의 멤버 함수 혹은 멤버 변수를 추가하는 것처럼 보일 수 있지만, 실제로는 단순히 새로운 함수를 호출할 수 있도록 만드는 것 뿐이다.
코드로 한번 살펴보자.
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
}
printClassName이라는 함수에 매개변수로 Shape 클래스 타입의 s를 받아서 getName method를 출력하도록 되어 있는데, Extension의 개념을 활용하지 않은 일반적인 클래스의 method 였다면 상속 받은 클래스에서 method를 정의할 때 override를 수행하라고 컴파일 에러를 발생 시켰을 것이다.
하지만 Extension의 경우 method가 static하게 resolve되기 때문에 printClassName의 인자가 Shape type일 경우 Shape를 확장하는 어떠한 서브 클래스를 인자로 넘겨줘도 "Shape" 로 동일하게 출력되는 것이다.
공식 문서에서는 다음과 같이 이유를 설명하고 있다.
This example prints Shape, because the extension function called depends only on the declared type of the parameter s, which is the Shape class.
그렇다면 Extension을 하고자 하는 method와 동일한 이름을 가진 멤버 함수가 클래스에 이미 존재한다면 어떨까?
fun main() {
class Example {
fun printFunctionType() { println("Class method") }
}
fun Example.printFunctionType() { println("Extension function") }
Example().printFunctionType()
// Class method
}
항상 멤버함수로 동작하게 된다.
하지만 overriding 개념과 유사하게 만약 Extension 하고자 하는 함수의 매개변수만 다를 경우에는 별개의 함수로 동작하게 된다.
Nullable Receiver
Extension은 Receiver가 Nullable한 객체도 허용한다. 코드로 바로 살펴보자.
fun main() {
fun Any?.toString(): String {
if (this == null) return "null"
return toString()
}
val nullableInt: Int? = 1
println(nullableInt.toString())
// 1
nullableInt = null
println(nullableInt.toString())
// null
}
nullable variable에 Extension function에서 null check를 수행할 수 있다.
Extension Properties
Property에 대해서 Extension을 진행하는 방법에 대해서 알아보자.
val <T> List<T>.lastIndex: Int
get() = size - 1
fun main() {
val list = listOf<Int>(1, 2, 3)
println(list.lastIndex)
// 2
}
조금 특이한 부분이 있다.
지금까지는 Extension이 main 문 안에 Local하게 존재하고 있었는데, 위 코드를 살펴보면 local code 바깥에 global하게 Extension이 일어난다.
공식 문서에서는 정확한 설명이 써있지는 않는데, main function 안에서 Propery Extension을 수행해보면 아래와 같은 에러가 발생하게 된다.
Local extension properties are not allowed
따라서 property를 extension 하는 경우에는 Global하게 적용해주도록 하자.
Companion object extensions
이번에는 Companion object에 대해서 Extension하는 예제를 살펴보자.
Companion object에 대해서도 property는 global로, function은 local로 extension이 가능하다.
class Example {
companion object {
}
}
val Example.Companion.text: String
get() = "example property"
fun main() {
fun Example.Companion.getExample() = "example"
println(Example.Companion.getExample())
// example
println(Example.Companion.text)
// example property
}
Declaring extensions as members
Extension을 클래스의 멤버 변수처럼 활용할 수 있다. 하나의 예시로 클래스의 extension을 다른 클래스에서 수행하는 것도 가능하다.
확장을 선언하는 클래스의 인스턴스를 디스패치 수신자라고 명칭하며, 확장 메서드의 수신자 유형 인스턴스를 확장 수신자 라고 명칭한다.
코드로 한번 살펴보자.
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()
//Host("kotl.in").printConnectionString() // error, the extension function is unavailable outside Connection
}
위 Connection 클래스에서 Host 클래스를 확장하는 printConnectionString 멤버 함수를 선언하는데, 여기서 printHostname은 Host 클래스가 암시적 수신자로 지정되며, printPort의 경우 Connection 클래스가 암시적 수신자로 지정된다.
위 코드에서 동작하는 것처럼 extension을 선언하는 코드에는 여러개의 암시적 수신자가 존재할 수 있다.
앞서 설명한 디스패치 수신자는 확장을 선언하는 Connection 클래스이며, 확장 수신자는 확장이 되는 대상인 Host 클래스가 된다.
암시적 수신자에 대한 추가적인 설명을 하나 더 해보자면, 만일 디스패치 수신자와 확장 수신자에 동일한 메서드가 있는 경우는 어떻게 될까?
아래의 예시를 통해 확인할 수 있다.
class Connection {
fun Host.getConnectionString() {
toString() // calls Host.toString()
this@Connection.toString() // calls Connection.toString()
}
}
기본적으로 확장 함수의 scope에서는 확장 수신자가 암시적으로 수신자로 지정되며, 이외의 수신자로 지정하고 싶은 경우 위와 같이 this + @className 으로 명시적으로 지정할 수 있다.
마지막 예시로 멤버 변수처럼 사용되는 Extension의 overriding 예제를 살펴보자.
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
}
멤버 변수로서 활용되는 Extension의 경우 open 키워드를 통해서 상속받은 서브 클래스에서 overriding이 가능하도록 할 수 있다.
main 함수에서도 설명하고 있고 공식 문서에서도 다음과 같이 위 코드를 설명하고 있다.
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.
필자가 위 의미를 이해하기로는
- BaseCaller().call(Base())
- DerivedCaller().call(Base())
위 두개의 코드 같이 dispatcher receiver에 따라서 call method가 불리는 경우 virtually working 한다는 것. 즉, dispatcher receiver에 dependent하게 작동한다는 것이다.
한편,
- DerivedCaller().call(Base())
- DerivedCaller().call(Derived())
와 같이 call method에 해당하는 매개변수는 method 헤더를 확인해보면 extension receiver인 Base 클래스인 것을 확인할 수 있다. 이는 곧 앞서 설명한 것처럼 resolve statically 하다는 것이다.
이번 블로그에서는 Kotlin의 Extension에 대해서 알아보았다.
나름 공부하다 보니 Android Programming 중에 유용하게 사용할 수 있을 것 같다는 생각이 들었다.
내용상의 문제가 있다면 언제든지 피드백 해주십시오.
'프로그래밍 언어 > Kotlin' 카테고리의 다른 글
Kotlin - Interface (2) | 2024.02.02 |
---|---|
Kotlin - Singleton Object (1) | 2024.01.31 |
Kotlin - 특수 클래스(data class, enum class, sealed class) (1) | 2024.01.30 |
Kotlin - Generic (1) | 2024.01.30 |
Kotlin - 람다 표현식 (0) | 2023.12.18 |