[Kotlin in Action] 9. 확장 함수와 확장 프로퍼티

반응형
728x90
반응형

확장함수

기존 자바 API를 재작성하지 않고도 자바 코드를 변환할 수 있어야한다. 어떤 클래스의 멤버 메서드인것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수다. 확장함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다.

- 객체 타입 : 클래스 이름
- 수신 객체 : 확장 함수가 호출되는 대상이 되는 값

확장 함수가 캡슐화를 깨지는 않는다. 클래스 안에서 정의한 메서드와 달리 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 비공개(private) 멤버나 보호된(protected) 멤버를 사용할 수 없다.

 

클래스의 멤버 메서드와 확장 함수를 모두 '메서드'라고 부르자. 클라이언트에게 자신이 호출하려는 메서드가 확장함수인지 멤버 메서드인지의 여부는 중요하지않다.

 

 

예제

  • String : 수신 객체 타입
  • this : 수신 객체
// String : 수신 객체 타입
// this : 수신 객체
fun String.lastChar() :Char = this.get(this.length - 1)

 

  • this 를 생략할 수 있다.
// this 생략 가능
fun String.lastChar2() :Char = get(length - 1)

 

  • 확장함수를 호출해보자.
fun main() {
    // 수신 객체 타입 : String
    // 수신 객체 : Kotlin

    // String 클래스에 새로운 메서드를 추가하는 것과 같다.
    println("Kotlin".lastChar())
}

 

  • JAVA에서의 호출 방식
// JAVA 에서 호출
char c = StringUtilKt.lastChar("Java");

 

 

확장함수 import

확장함수를 정의했다고 해도 자동으로 프로젝트 안의 모든 소스코드에서 그 함수를 사용할 수 있지는 않다.

그 함수를 다른 클래스나 함수와 마찬가지로 import 해야한다.

import strings.lastChar

fun main() {
    // 수신 객체 타입 : String
    // 수신 객체 : Kotlin

    // String 클래스에 새로운 메서드를 추가하는 것과 같다.
    println("Kotlin".lastChar())
}

 

1) as 사용

확장 함수를 정의하자마자 어디서는 그 함수를 쓸 수 있다면 한 클래스에 같은 이름의 확장 함수가 둘 이상 있어서 이름이 충돌하는 경우가 자주 생길 수 있다. as 를 사용해서 이름이 같은 함수의 경우 이름 충돌을 막을 수 있다. 코틀린 문법상 확장 함수는 반드시 짧은 이름을 써야한다.

import strings.lastChar as last // as 를 사용해서 다른 이름으로 부를 수 있다.

 

 

유틸리티 함수 정의

  • Collectioon<T>에 대한 확장 함수를 선언해보자. 
/*
Collection<T> 에 대한 확장 함수를 선언한다.
 */
fun <T> Collection<T>.joinToString(
    // default 값 지정한다.
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
): String {
    val result = StringBuilder(prefix)

    // 수신 객체 : this
    // T 타입의 원소로 이루어진 컬렉션이다.
    for ((index, element) in this.withIndex()) { // this는 수신 객체(= T타입 컬렉션)
        if (index > 0) result.append(separator)
        result.append(element)
    }

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

1) 호출

  • list, arrayList 모두 가능하다.
fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(list.joinToString(separator = "; ", prefix = "(", postfix = ")"))

    val arrayList = arrayListOf(1, 2, 3)
    println(arrayList.joinToString(" "))
}

 

확장 함수는 단지 정적 메서드 호출에 대한 문법적인 편의다. 그래서 클래스가 아닌 더 구체적인 타입을 수신 객체 타입으로 지정할 수도 있다. 문자열의 컬렉션에 대해서만 호출할 수 있는 Join 함수를 정의해보자.

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

 

 

확장함수 오버라이드 불가

코틀린의 메서드 오버라이드도 일반적인 객체지향의 메서드 오버라이드와 마찬가지다. 하지만 확장 함수는 오버라이드 할 수 없다.

 

기본 오버라이드 방식

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

/*
Button은 View의 하위 타입이 된다.
View 타입 변수를 선언해도 Button 타입 변수를 그 변수애 대입할 수 있다.
 */
class Button: View() { // Button 이 View 를 확장한다.
    override fun click() {
        println("Button Checked")
    }
}

 

  • 호출
fun main() {
    val view: View = Button()
    view.click() // view 에 저장된 실제 타입에 따라 호출할 메서드가 결정된다. (Button.click())
}

 

 

확장함수 오바라이드 불가

확장 함수는 클래스의 일부가 아니다. 확장 함수는 클래스 밖에 선언된다. 이름과 파라미터가 완전히 같은 확장 함수를 기반 클래스와 하위 클래스에 대해 정의해도 실제로는 확장 함수를 호출할때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정된다. 그 변수에 저장된 객체의 동적인 타입에 의해 확장 함수가 결정되지 않는다.

// 확장함수 선언
fun View.showOff() = println("view!")
fun Button.showOff() = println("button!")

 

  • 호출
fun main() {
    // 확장함수 호출
    val view2: View = Button()
    /*
        확장 함수는 정적으로 결정된다. View.showOff()
     */
    view2.showOff() // view!
}

 

1) 확장 함수는 정적으로 결정된다. 

View.showOff()

 

view2 가 가리키는 객체의 실제 타입이 Button 이지만, 이 경우 view2이 타입이 View이기 때문에 View의 확장함수가 호출된다. 확장 함수는 첫번째 인자가 수신 객체인 정적 자바 메서드로 컴파일한다. 코틀린은 확장 함수를 정적으로 결정하므로 오버라이드 할 수 없다.

 

 

 

확장 프로퍼티

확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다.
프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 적절한 방법이 없기 때문에, 실제로 확장 프로퍼티는 아무 상태도 가질 수 없다.
(= 기존 클래스의 인스턴스 객체에 필드를 추가할 방법은 없다.)

하지만 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어서 편한 경우가 있다.

 

  • get 정의
val String.lastChar: Char
    // 최소한 게터는 꼭 정의해야한다.
    get() = get(length - 1)

 

  • 호출
fun main() {
    println("Kotlin".lastChar)
}

 

  • get, set 정의
var StringBuilder.lastChar2: Char
    // 최소한 게터는 꼭 정의해야한다.
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }

 

  • 호출
fun main() {
    val sb = StringBuilder("Kotlin?")
    sb.lastChar2 = '!'
    println(sb) // Kotlin!

}

 

  • JAVA에서의 호출 방식
// JAVA (Get, Set 을 명시적으로 호출)
StringUtilkt.getLastChar("Java")

 

 

반응형

Designed by JB FACTORY