본문 바로가기
2023년 이전/kotlin

Kotlin - 확장 함수 와 확장 프로퍼티

by JeongUPark 2020. 2. 19.
반응형

[출처 -  Kotlin In Action] [아래 내용들은 Kotlin In Action을 공부하면서 스스로 정리한 내용입니다] 

확장함수

kotlin에는 확장함수라는 기능이  있습니다. 확장함수는 클래스의 멤버 메소드인 것 처럼 호출 할 수 있지만 그 클래스 밖에 선언된 함수 입니다.

 

그럼 예제로 확장 함수를 알아 봅시다. 마지막 글자를 획득하는 확장 함수를 만들어 보겠습니다.

확장 함수를 만드려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이면 됩니다.

fun main(args: Array<String>) {
    println("Kotlin".lastChar())
}
fun String.lastChar():Char = this.get(this.length-1)

위 code에서 String.lastChar()이 확장 함수 입니다. 저 code를 통하여 확장함수에 대하여 설명을 드리면

fun String.lastChar():Char = this.get(this.length-1)

에서 String이 수신 객체 타입(위 설명에서 클래스 이름입니다.), this가 수신객체(확장 함수가 호출되는 대상이 되는 값)를 의미합니다. 그래서 String이 수신객체 타입이기 때문에 "Kotlin"이 수신객체가 될 수 있습니다.

 

그리고 일반 메소드의 본문에서 this를 사용할 때와 마찬가지로 확장함수 본문에서도 this를 사용가능하고, 일반 메소드에서 this를 생략할 수 있는 것 처럼 확장함수 본문에서도 this를 생략 할 수 있습니다.

fun String.lastChar():Char = get(length-1)

또한, 클래스 안에서 정의한 메소드와 달리 확장 함수 안에서는 클래스 내부에서만 사용할 수 잇는 비공개(private) 멤버나 보호된(protected) 멤버를 사용할 수 없습니다.

 

확장함수 호출

확장 함수를 사용하려면 파일 내부에서는 따로 설정 없이 사용할 수 있지만, 그 파일 이외의 동일 프로젝트 안이라도 사용할 수 있는 것은 아닙니다. 

사용을 위해서는 임포트가 필요 합니다. code를 통해 확인해 보겠습니다.

 

ExtentionFun.kt라는 파일을 만들고 다음과 같이 작성합니다.

package strings

fun String.lastChar():Char = get(length-1)

그리고 확장함수 lastChar을 사용할 파일에 들어가서 다음과 같이 작성합니다.(저는 Test_ExtentionFun.kt 라는 파일을 만들었습니다.)

import strings.*
fun main(args: Array<String>) {
    println("Kotlin".lastChar())
}

import strings.*을 통하여 확장 함수를 사용할 수 있도록 합니다. .*을 하면 pakage strings에 있는 함수들을 사용할 수  있습니다. import strings.* 말고도 import strings.lastChar를 사용해도 확장함수 lastChar() 을 사용할 수 있고 또한 

import strings.lastChar as last
fun main(args: Array<String>) {
    println("Kotlin".last())
}

위와 같이 as last를 붙여서 lastChar을 last로 변경하여 사용이 가능합니다. 이렇게 하는 이유는 확장함수의 이름 충돌을 피하는 방법입니다.

Java에서는 

import strings.ExtentionFunKt;
public class Test_ExtentionFun {

    public static void main(String[] args){
        char c = ExtentionFunKt.lastChar("kotlin");
    }

}

이렇게 사용할 수 있습니다.

확장함수로 유틸리티 함수 정의

그럼 이전에 만든 joinToString 함수를 이용하여 확장함수에 대해서 더 확인해 보겠습니다. (joinToString 함수는 여기서 확인 할 수 있습니다.)

joinToString의 Collection<T>을 수신타입으로 확장함수를 만들면

package extention_test
fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val list = arrayListOf(1, 2, 3)
    println(list.joinToString(" "))
}

 이렇게 확장함수를 만들어 사용할 수 있습니다. 이런 확장함수는 단지 정적 메소드 호출에 대한 문법적인 편의일 뿐입니다. 그래서 다음과 같이 사용도 가능 합니다.

import extention_test.joinToString

fun Collection<String>.join(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
) = joinToString(separator, prefix, postfix)

fun main(args: Array<String>) {
    println(listOf("one", "two", "eight").join(" "))
}

 위에서 사용된 joinToString은 그 위에서 작성한 Collection<T>를 수신타입으로 하는 확장함수입니다. 그리고 위에서 Collection<String>으로 하였기 때문에 만일 listOf(1,2,3)을 할 경우 Type mismatch라고 code에 error가 발생합니다.(만일 kotlin REPL에서 실행을 하면 error: type mismatch: inferred type is List but Collection was expected 라는 결과를 볼 수 있습니다.)

확장함수는 오버라이드 할 수 없다

 

자 다음 코드를 보겠습니다.

open class View {
    open fun click() = println("View clicked")
}

class Button: View() {
    override fun click() = println("Button clicked")
}

fun main(args: Array<String>) {
    val view: View = Button()
    view.click()
}

 Button이 View를 상속 받아 Button은 View의 자식 클래스가 됩니다. 그래서 위의 main에서 처럼 view에 Button 을 대입할 수 있습니다. 그리고 실행을 하면

Button clicked

View 타입의 click이 실행될 것 같지만 Button이 View의 click을 오버라이드 했고 view는 Button으로 생성되었기 때문에 Button의 click이 동작합니다.

 

하지만 확장함수는 이런 오버라이드를 사용 할 수 없습니다. 다음을 보겠습니다.

open class View {
    open fun click() = println("View clicked")
}

class Button: View() {
    override fun click() = println("Button clicked")
}

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

fun main(args: Array<String>) {
    val view: View = Button()
    view.showOff()
}

 

위의 View와 Button이외에 showOff라는 확장함수를 각각을 수신타입으로하여 생성하였습니다. 그리고 main에서 view를 Button으로 생성 후 showOff를 실행 시키면 

I'm a view!

라는 결과가 나옵니다. 만일 main을

fun main(args: Array<String>) {
    val view: Button = Button()
    view.showOff()
}

이렇게 바꾸면 I'm a button! 이라는 결과가 나올 것입니다. 그 이유는 확장함수는 클래스의 일부가 아니고 클래스 밖에 선언됩니다. 그래서 수신객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출 될지 결정됩니다. 그래서 첫번쨰 main에서는 수신객체 타입이 View여서 I'm a view!라는 결과가 두번째 main 에서는 수신 객체의 타입이 Button이어서 I'm a button!이라는 결과가 나타났습니다.

 

그리고 어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 같다면 확장함수가 아니라 멤버 함수가 호출됩니다(멤버 함수의 우선순위가 높습니다)

 

확장 프로퍼티

확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있습니다. 사실 확장 프로퍼티는 아무런 상태를 가질 수 없지만, 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있습니다. ( 사실 글로는 무슨 뜻인지 확 와닿지가 않는다. 그러니 코드로 확인해보게습니다.)

 

val String.lastChar: Char
    get() = get(length - 1)
var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }

다음과 같이 String과 StringBuilder에 대한 확장 프로퍼티를 만들었습니다. 위에서 보면 알 수 있듯이, 일반 프로퍼티와 같지만 확장함수처럼 수신객체가 추가 되었습니다. 이런 확장 프로퍼티는 기본적으로 getter는 꼭  구현해야 합니다. 또한, 초기화 코드에서 계산한 값을 담을 장소가 없으므로 초기화 코드도 쓸 수 없습니다.

 

그럼 위의 확장 프로퍼티를 사용하면

val String.lastChar: Char
    get() = get(length - 1)
var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }

fun main(args: Array<String>) {
    println("Kotlin".lastChar)
    val sb = StringBuilder("Kotlin?")
    sb.lastChar = '!'
    println(sb)
}
n
Kotlin!

이런 결과를 확인 할 수 있습니다.

 

그리고 자바에서 사용하려면

import ExtensionProperties.ExtensionPropertiesKt;
public class Test_Join {

    public static void main(String[] args){
        char c = ExtensionPropertiesKt.getLastChar("Java");
    }

}

이렇게 게터나 세터를 명시적으로 표현해 주어야 합니다. (당연히 위에서 확장프로터티를 가진 파일 이름은 ExtensionProperties.kt이고 pakege는 ExtensionProperties 입니다.)

반응형