본문 바로가기

코틀린

코틀린: 확장 함수와 수신 객체를 갖는 함수 타입

확장 함수(extension function)에 대한 보충 설명입니다. 자세한 내용은 쉽게 다가가는 최신 프로그래밍: 코틀린 - 3.2 함수형 프로그래밍, 3.4 람다 식: 고급 3.6.2 확장 함수를 참고하기 바랍니다. 확장 함수와 관련된 내용이 이렇게 많았다구요? 놀랍죠! 4.16 범위 함수(scope function) 에서 소개한 5개 범위 함수 중 with를 제외한 4개(also, apply, let, run)가 모두 확장 함수입니다. 

확장(extension)이란 기능을 추가한다는 뜻입니다. 정확히 말하면 기존 클래스(class)에 기능을 추가한다는 뜻입니다. 클래스에서 제공하는 함수 대신 꼭 필요한 기능을 사용 목적에 맞게 추가하는 겁니다. 클래스에 추가하는 기능은 대부분 함수이지만 속성도 추가할 수 있습니다. 를 사고 나서 내 스타일에 맞게 튜닝하는 것과 같습니다. 확장 함수와 2.2 연산자 오버로딩을 헷갈리면 안됩니다. 연산자 오버로딩에서는 연산 기호(*)와 이 기호에 대응되는 함수 이름(times)이 1:1 관계로 정해져 있습니다.

책 여기 저기에 흩어져 있던 확장 함수에 관련된 내용을 한 군데 모았습니다. 물론, 확장 속성도 가능하지만, 확장 함수보다 상대적으로 사용 빈도가 적습니다. 여기서는 확장 함수에만 초점을 맞췄습니다.

■ 확장 함수(extension function)
확장 함수는 기존 클래스에 새 멤버 함수를 추가한 것과 같습니다. 확장 함수 안에서는 클래스의 멤버 함수처럼 수신 객체(receiver)를 this로 참조할 수 있습니다. 아래 예제는 Int 클래스에 확장 함수 multiple()을 추가했습니다. 이 함수의 기능은 수신 객체에 형식 인자를 곱한 결과를 반환합니다. 

fun Int.multiple(a: Int): Int {
    if (a < 0) return -1
    if (a == 0) return this
    return this * a
}

fun main() {
    println(1.multiple(3)) // 1*3 = 3
    println(5.multiple(10)) // 5*10 = 50
}

 

■ 확장 함수: 기존 클래스의 메소드를 참조
기존 클래스의 메소드를 참조하는 방법도 있습니다. fun이 아닌 val로 선언해서 당황했나요? intMultiple을 함수 타입으로 선언했기 때문에,  intMultiple은 당당히 함수를 참조할 수 있습니다. 함수 타입 Int.(Int)에서 왼쪽 Int가 수신 객체입니다. 이렇게 지정한 것을 수신 객체를 갖는 함수 타입이라고 합니다. 일반 함수를 참조할 때는 ::함수_이름 이지만, times가 Int 클래스의 멤버 함수이기 때문에 Int::times를 사용합니다. 수신 객체를 갖는 함수 타입은 1.intMultiple(3) 또는 일반 함수처럼 intMultiple(1, 3)로도 사용할 수 있습니다.

val intMultiple: Int.(Int) -> Int = Int::times

fun main() {
    println(1.intMultiple(3))
    println(intMultiple(1, 3))
    
    println(5.intMultiple(10))
    println(intMultiple(5, 10))
}

 

■ 확장 함수: 수신 객체를 갖지 않는 함수 타입
함수 타입이 수신 객체를 갖는 것과 갖지 않는 것은 어떤 차이가 있을까요? 아주 뚜렷한 차이점이 있습니다. 아래 예제에서 쉽게 확인할 수 있습니다. 수신 객체가 없기 때문에 1.intMultiple(3)과 같은 표현은 사용할 수 없습니다.

val intMultiple: (Int, Int) -> Int = Int::times

fun main() {
    println(1.intMultiple(3)) // 에러 발생
    println(intMultiple(1, 3))
}

 

■ 확장 함수: 람다 식 및 익명 함수 사용
함수 타입 파라미터를 갖는 확장 함수에 람다 식을 사용할 수 있습니다. 아래 람다 식은 형식 인자가 하나뿐이어서 간단히 { this * it } 로 쓸 수 있습니다.

val intMultiple: Int.(Int) -> Int = { a: Int -> this * a } // { this * it } 

fun main() {
    println(1.intMultiple(3))
    println(intMultiple(1, 3))

    println(5.intMultiple(10))
    println(intMultiple(5, 10))
}

람다 식 대신 아래처럼 익명 함수를 사용해도 됩니다.

val intMultiple: Int.(Int) -> Int = fun Int.(a: Int) = this * a

 

■ 확장 함수: 기존 클래스의 멤버 함수로 추가
3.6.2절(p.151)의 예제에서는 클래스 Student에 대해 확장 함수 isScholarship()을 정의했습니다. 확장 함수도 클래스의 멤버 함수와 마찬가지 방법(kim::isScholarship)으로 참조할 수 있습니다.

class Student(val name: String) {
    fun hasPassed(score: Int): String =
        if (score > 60) "Passed" else "Failed"
}

fun Student.isScholarship(score: Int): Boolean {
    return score > 85
}

fun main() {
    val kim = Student("Kim")
    val status = kim::isScholarship
    println(status(78))
}

확장 함수 isScholarship()을 기존 클래스 Student의 멤버 함수로 포함할 수 있을까요? 가능합니다. 멤버 함수와 확장 함수의 차이점은 클래스 이름이 있고 없고의 차이입니다. 그렇다면 굳이 포함시킬 필요가 있을까? 

class Student(val name: String) {
    fun hasPassed(score: Int): String =
        if (score > 60) "Passed" else "Failed"
    fun Student.isScholarship(score: Int): Boolean {
        return score > 85
    }
}

 

필요할 때가 있습니다. 확장 함수이면서 멤버 함수가 된 isScholarship()은 클래스 Student의 private 멤버에 접근할 수 있는 권한을 갖습니다. isScholarship()은 private 멤버로 선언한 속성 grade에 자유롭게 접근할 수 있습니다. 

class Student(val name: String) {
    private var grade: Char = 'F'
    fun hasPassed(score: Int): String =
        if (score > 60) "Passed" else "Failed"
    fun Student.isScholarship(score: Int): Boolean {
        if (score >= 90) grade = 'A'
        else if (score in 85..89) grade = 'B'
        return score > 85
    }
}

 

■ 범위 함수(scope function)는 확장 함수
run, also, apply, let 등의 범위 함수는 모두 확장 함수입니다. 범위(scope)란 용어를 사용하는 것은 적용 범위를 확장 함수의 형식 인자인 람다 식 블록으로 제한하기 때문입니다. 범위 함수의 구조는 수신_객체.범위_함수 { ... } 입니다. 예들 들면, run() 함수의 형식 인자가 람다 식 { ... } 입니다. 람다 식 블록에 수신 객체(this) 또는 형식 인자(it)가 전달되는 형태입니다. 범위 함수에 관한 자세한 설명은 4.16절을 참고하기 바랍니다.