본문으로 바로가기
반응형


이 글은 Kotlin In Action을 참고 하였습니다.

더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다

3.3 확장함수

확장함수 (Extension Function)는 클래스의 멤버 메서드처럼 호출되지만 클래스 밖에 호출되는 함수 입니다.
앞서 포스팅에서 joinToString() 함수로 예를 들어서 봤었죠.

간단하게 String에 마지막 값을 반환하는 함수를 String class에 추가하려면 아래와 같이 할 수 있습니다.
package strings

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


특정 클래스에 확장 함수를 추가하려면 위 예제처럼 function을 만들때 앞에 해당 class를 붙여주면 됩니다.

  • Receiver type: 확장이 정의될 클래스 ex) String
  • Receiver object: 그 클래스의 인스턴스 객체 ex) this

이 함수를 사용할 때는 아래와 같이 사용하면 됩니다.

println("Kotlin".lastChar())

-> n

확장함수는 마치 기본 클래스에 추가적으로 함수를 넣는 기능을 합니다.

여기서 Receiver object인 this는 다른부분과 마찮가지로 생략 가능하므로 아래와 같이 줄여서 쓸수도 있습니다.

package strings

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

단! 확장함수는 receiver object의 private이나 protected함수에는 접근할 수 없습니다.

확장함수는 엄밀히 외부에서 해당 object에 접근하는것이므로 해당 class의 public 함수에만 접근이 가능합니다.


3.3.1 확장함수의 import

또한 String class만 import해서는 해당 함수를 사용할 수 없습니다.

아래처럼 명시적으로 import해야만 사용이 가능합니다.

import strings.lasChar

val c = "Kotlin".lastChar()

import strings.*

val c = "Kotlin".lastChar()

import strings.lasChar as last

val c = "Kotlin".last()

마지막 함수는 as를 이용하여 함수 이름을 변경했습니다.

만약 사용하는 class에서 import한 함수들이 확장함수와 같은 이름의 함수가 있을경우 last()라는 이름으로 치환하여 사용할때 유용한 기능입니다.


3.3.2 Java에서의 호출

내부적으로 확장함수는 receiver object를 첫번째 인자로 받는 정적 메소드 입니다.
따라서 자바에서 호출시 receiver object를 첫번째로 인자로만 넘겨주면 쉽게 호출이 가능합니다.
 // 자바
char c = StringUtilKt.lastChar("Kotlin");


3.3.3 확장 함수를 util로 정의

맨 처음 확장 함수를 언급하면서 util함수를 대체할 수 있다라고 했었습니다.
실제 앞 포스팅에서 만들었던 joinToString()을 util로 만들어 보겠습니다.
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) {
    val list = listOf(1, 2, 3)
    println(list.joinToString(separator = "; ", prefix = "(", postfix = ")"))
    
    val list = arrayListOf(1, 2, 3)
    println(list.joinToString(" "))
}

joinToString 함수가 이제 collection의 확장함수가 되었으니, 관련 class들에서는 receiver object에서 직접 함수를 호출할 수 있습니다.

마치 collection에 포함되어 있던것처럼 말입니다.


만약 collection에서 더 specific하게 특정 type만 정의하고 싶다면 generic 대신 특정 형태를 넣어서 구현해도 됩니다.

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

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


3.3.4 확장함수의 override

확장함수는 overriding을 할 수 없습니다.
확장함수가 static의 성격을 띄는 함수라는걸 다시한번 상기한다면 당연한 얘기입니다.

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

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

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

View class와 Button 클래스를 정의하고 click()이란 함수를 override했다고 하면,

위 코드의 결과는 "Button clicked"가 나옵니다.

자바와 동일하죠!


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) {
    val view: View = Button()
    view.showOff()
}

여기에 showOff()라는 확장함수를 View와 Button에 모두 추가했습니다.

이때 main 함수의 결과로는 "I'm a view!" 가 출력됩니다.

Button 객체가 View에 담겨있지만 확장함수는 view에 정의된게 호출된다는걸 명심해야 합니다.


추가적으로, 특정 클래스에 확장함수를 추가 할 때 이미 같은 이름의 멤버변수가 있다면 확장함수를 항상 무시됩니다.

즉 같은 이름이라면 멤버변수 > 확장함수의 우선순위를 가집니다.

3.3.5 Extension property

확장 prpperty도 확장 함수처럼 추가할 수 있습니다.
다만 확장 property는 상태를 저장할 수 없기 때문에 단순히 확장함수를 좀 짧게 만드는 효과정도를 가집니다.
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) {
    println("Kotlin".lastChar)
    val sb = StringBuilder("Kotlin")
    sb.lastChar = '!'
    println(sb)
}

확장된 property는 backing field를 가지고 있지 않습니다.

따라서 기본 getter를 가질수 없으므로 최소한 getter는 구현해야 합니다!!

위 함수의 결과로는 n, Kotlin! 이 출력됩니다.
 // 자바에서 호출시
StringUtilKt.getLastChar("Java");


3.4 컬렉션의 처리.

3.4.1 자바 Collection 확장 API

위에서 확장함수 만드는법과 추가하는법을 확인했습니다.
코틀린에서는 여러 유용함 함수들을 추가해 놓았으니, IDE의 자동완성 기능을 이용하면 편리하게 사용할 수 있습니다.
last()나 max() 처럼 예상가능한것도 있지만, 하나하나 다 외워서 쓸수는 없기 때문에 "혹 이런 API 있나?" 싶으면 IDE 자동완성기능을 사용하는걸 추천드립니다.

3.4.2 가변인자 (vararg)

자바에서는 가변인자 사용으 위해서 "..."이란 키워드를 사용합니다.
Kotlin에서는 vararg 키워드를 사용하며, 파라미터 앞에 붙여서 쓰면 됩니다.
fun listOf<T> (vararg values:T): List<T>{...}


또한 배영을 추가할때 자바와는 달리 spread 연산자인 *을 사용해서 원소를 풀어서 넣을 수 있습니다.

fun main(args: Array) {
    val list = listOf("args: ", *args)
    println(list)
}

위 코드는 list에 "args:"와 전달받은 array를 풀어서 넣습니다.


3.4.5 infix function과 destructuring

map을 만들때 to 라는 문자를 사용했었습니다.
이는 키워드가 아니라 사실 중위함수 (infix function)입니다.
 infix fun Any.to(other: Any) = Pair(this, other)

to 함수는 이렇게 정의되어 있습니다.

Any는 자바의 Object라고 생각하면 됩니다.

즉 어떤 object든 to를 쓸수 있으며, to를 쓰면 pair 객체를 반환해 줍니다.


받는 인자가 한개의 경우 infix function으로 만들 수 있습니다.

위 예제처럼 함수 앞에 infix를 붙이면 되며, infix가 붙이면 사용할 때 아래와 같이 표기할 수 있습니다.

1 to "one" // infix 함수에서 허용하는 형태
1.to("one") // 일반 함수처럼 사용하는 형태

to라는 infix function으로 생성된 pair는 destructuing을 지원합니다.

따라서 두개의 변수를 동시에 초기화하는게 가능합니다.

 val (index, name) = 1 to "one"

일반적인 destructuring의 사용방법은 추후에 포스팅 하겠습니다. (한참 뒤에나 나옵니다.^^a)

fun <K,V> mapOf(vararg values:Pair<K,V>): Map<K,V>

mapOf는 위와같이 정의되어 있습니다.

따라서 destructuring을 통해서 아래와 같이 iteration이 가능했습니다.

for ((key, value) in sampleMap) {
    ...
}

for ((index, value) in collection.withIndex()) {
    ...
}

반응형