Kotlin

확장함수 (Extension Function)

kakaroo 2022. 4. 14. 21:12
반응형

article logo


 

Java에서는 상속해서 overriding을 통해 함수를 재정의해서 사용하였습니다.

코틀린에서는 상속을 통한 확장과는 다른 방법으로 클래스를 확장합니다.

 

fun 클래스이름.메소드이름(매개변수...) {   //확장하게 되는 클래스를 receiver type 이라고 한다.

   // this 로 받을 수 있다. 이를 receiver object 라고 한다.

}

 

fun main(args: Array<String>) {
    val l = mutableListOf(1, 2, 3)
    l.swap(1,2)
}

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

 

타입을 Generic 으로 선언하면 다음과 같습니다.

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

 


클래스 멤버의 함수와 확장함수의 이름과 파라미터가 모두 같은 경우는 클래스 멤버 함수가 우선순위를 갖습니다.

class Test {
    fun PPP() {
        print("member function")
    }
}
fun Test.PPP() {
    print("extension function")
}

fun main(args: Array<String>) {
    val test = Test()
    test.PPP()
}

//output : member function

 


 

확장함수는 정적 바인딩 됩니다.

  • 정적 바인딩 : 함수 호출 부분에 메모리 주소 값을 저장하는 작업이 컴파일 시간 에 행해지기 때문에 컴파일 이후로 이 값이 변경되지 않는다.
  • 동적 바인딩 : 함수 호출 부분에 메모리 주소 값을 저장하는 작업이 컴파일 시간에는 보류되고, 런타임 에 결정된다.

 

아래 Parent class를 상속받은 Child class가 있습니다. 확장함수를 동일한 함수 이름으로 생성하고,

printObjectName 함수는 부모 클래스인 Parent class를 매개변수로 받습니다.

main 함수에서 Child 클래스를 매개변수로 받는 printObjectName 함수를 호출합니다.

 

open class Parent
class Child : Parent()

fun Parent.getObjectName() = "parent"
fun Child.getObjectName() = "child"

fun printObjectName(p: Parent) = println(p.getObjectName())

fun main(args: Array<String>) {
    printObjectName(Child())
}

 

생각해보면, Child의 확장함수인 Child.getObjectName()가 호출되어 child가 출력되어야 하지만,

위에 설명드린데로 확장함수는 정적바인딩되어 컴파일 시간에 printObjectName 함수는 이미 Parent 객체의 확장함수의 메모리 주소로 결정되어 버립니다.

출력값 : parent

 


그렇다면 어떠한 경우에 사용을 할까요?

3rd party 라이브러리에 아래와 같은 data class 가 정의되어 있다고 가정하면,

해당 class를 상속해서 함수 생성하는 게 아니라, 코틀린에서는 확장함수로 만들어 응용을 할 수 있습니다.

더군다나, 코틀린에서는 디폴트로 상속이 안 되게 되어 있습니다. class 앞에 open 키워드가 붙지 않으면 상속이 되지 않습니다. 디폴트로 상속이 되는 자바와는 다른 개념이지요. (* 자바는 final 키워드로 상속을 금지합니다)

 

data class Order(val items: Collection<Item>)
data clsss Item(val name: String, val price: Int)

private fun Order.maxPricedItemName() = this.items.maxByOrNull { it.prices } ?.name ?: "no product"

 


 

확장 프로퍼티

클래스의 함수만 확장하는 게 아니라, 클래스의 프로퍼티도 확장이 가능합니다.

단, 초기화는 반드시 get() 함수로 해야 합니다.

private val Order.itemNames : String
	get() = items.map { it.name }.joinToString("")

 

 


정적 바인딩의 이해를 돕기 위한 상속과 확장의 차이

 

//상속의 예
open class Super {
	open fun sayHello() {
    	println("Super -> sayHello()")
	}
}

class Sub : Super() {
	override fun sayHello() {
    	println("Sub -> sayHello()")
    }
}

fun some(obj: Super) {
	obj.sayHello()
}

som(Sub())

출력: Sub -> sayHello()

 

some() 함수의 매개변수가 Super로 선언되어 있더라도 실제 이 매개변수에 대입되는 객체는 Sub 타입입니다.

대입되는 객체가 Sub 타입이므로 호출되는 함수는 Sub 클래스의 sayHello() 입니다.

---> 상속 : runtime 때 결정되는 동적 바인딩

 

 

다음은 확장 함수의 예입니다.

//상속 관계에서 확장 함수를 추가
open class Super

class Sub: Super()

fun Super.sayHello() {
	println("Super -> sayHello()")
}

fun Sub.sayHello() {
	println("Sub -> sayHello()")
}

fun some(obj: Super) {
	obj.sayHello()
}

some(Sub())

출력: Super -> sayHello()

 

some() 함수의 매개변수는 Super 클래스이고 본문에서 호출한 sayHello 함수는 확장함수이므로 컴파일 타임에 Super 클래스의 sayHello() 확장함수로 결정이 됩니다.

---> 확장함수 : compile 때 결정되는 정적 바인딩

 

 


 

 

확장 구문은 대부분 최상위 레벨에 작성합니다. 최상위 레벨에 선언한 확장 함수나 프로퍼티를 같은 파일에서 사용하는 것은 문제가 없지만, 외부 파일에서 이용할 때는 별도로 import를 받아서 사용해야 합니다.

 

아래 예를 들어 보겠습니다.

package com.test.one

class Test {
	val data1 : Int = 10
}

val Test.data2: Int
	get() = 20
    
fun main(args: Array<String>) {
    val obj = Test()
    println(obj.data2)
}

----------------------------------

package com.test.two
package com.test.one.Test	//외부 클래스를 import

fun main(args: Array<String>) {
    val obj = Test()
    println(obj.data1)
    println(obj.data2)	//compile error
}

data2 라는 확장 프로퍼티가 com.test.one package에 선언되어 있습니다. com.test.two package에서 Test class를 사용하기 위해 com.test.one.Test 클래스를 import 했습니다. 클래스 내에 선언한 data1 프로퍼티에 접근하는 데는 아무 문제가 없지만, 확장으로 추가한 data2 프로퍼티에 접근시 에러가 발생합니다.

에러가 발생하는 이유는 확장된 함수나 프로퍼티가 실제 Test 클래스 내에 작성되는 것이 아니라 클래스 외부에 작성되기 때문입니다. 그러므로 Test를 import 했다고 외부에 선언된 Test 의 확장함수나 프로퍼티까지 import 되지는 않습니다.

 

에러를 해결하기 위해서는 클래스를 import 하듯이 확장된 함수나 프로퍼티를 별도로 import 하면 됩니다.

--> import com.test.one.data2

 

 


확장구문은 다른 클래스 내에 작성할 수도 있습니다. 이 때 확장대상이 되는 클래스를 extension receiver 라고 부르고, 확장구문이 작성된 클래스를 dispatch receiver 라고 부릅니다.

 

클래스내에서 다른 클래스의 함수를 확장할 경우 확장한 클래스의 함수에 접근할 수 있습니다.

하지만, 외부에서 확장함수가 포함된 클래스 객체를 사용할 경우, 확장함수가 있더라도 해당 클래스에 접근할 수는 없습니다.

package com.test.three

class ExtensionClass {
	fun some1() {
    	println("ExtensionClass some1()")
    }
}

class DispathClass {
	fun dispatchFun() {
    	println("DispathClass dispatchFun()")
    }
    
    fun ExtensionClass.some2() {
    	some1()			//Extension receiver의 함수 호출
        dispatchFun()	//Dispatch receiver 내에서 선언되었으므로 가능
    }
    
    fun test() {
    	val obj: ExtenstionClass = ExtensionClass()
        obj.some1()
        obj.some2()        
    }
}

fun main(args: Array<String>) {
	val obj: ExtenstionClass = ExtensionClass()
    obj.some1()
    obj.some2()	// error ; 확장구문이 DispatchClass 내부에 정의된 것이므로 DispatchClass 내부에서만 확장함수가 적용됨
}
반응형

'Kotlin' 카테고리의 다른 글

Animation  (0) 2022.04.18
object, companion object  (0) 2022.04.15
코루틴(Coroutine)  (0) 2022.04.03
BlogViewer (feat. JSoup Crawling)  (1) 2022.04.03
inline 함수  (0) 2022.04.01