본문 바로가기

코틀린

코틀린: 멤버 참조

멤버 참조(member reference)에 관한 보충 설명입니다. 자세한 내용은 쉽게 다가가는 최신 프로그래밍: 코틀린 - 3.2 함수형 프로그래밍, 3.3 람다 식: 기초를 참고하기 바랍니다.

멤버 참조란 이중 콜론(::)을 사용한 식입니다. "멤버 참조"란 용어 때문에 클래스의 멤버(메소드나 속성)만을 참조하는 것으로 오해하면 안됩니다. 가장 궁금한 건 이거겠죠. "클래스 멤버는 점 표기법(dot notation)을 사용해 참조하면 되는 데, 굳이 멤버 참조를 사용하는 이유가 뭔가요?함수를 예로 들면, 멤버 참조는 함수를 호출하는 것이 아니라 함수를 참조(refer)합니다. 함수를 호출하면 함수를 즉시 실행하고 결과를 반환합니다. 반면, 함수를 참조하면 함수를 실행하지 않습니다. 

멤버 참조는 1. 상위 레벨 함수 참조, 2. 클래스의 멤버(메소드나 속성) 참조, 3. 확장 함수 참조 , 4. 클래스의 생성자 참조 등 4가지 형태로 사용할 수 있습니다.  생각했던 것보다 다양한 참조가 가능합니다.

■ 상위 레벨 함수 참조
상위 레벨 함수(클래스의 멤버 함수가 아님) multiply()를 참조할 때 이중 콜론(::)을 사용해  이 함수를 참조합니다.

fun multiply(a: Int, b: Int): Int = a * b  // 상위 레벨 함수

fun main() {
    val referencingFunc: (Int, Int) -> Int = ::multiply  // 멤버 참조 - 상위 레벨 함수
    println(referencingFunc(4, 5)) // 20
}

 

■ 클래스 멤버 참조 : 속성(property)
클래스의 멤버 중 속성은 "클래스_이름::속성_이름"처럼 참조할 수 있습니다. 변수 nameReference에 멤버 참조(Person::name)를 할당하고, getter를 사용해 인스턴스 kim의 name 속성 값("Kim")을 가져올 수 있습니다. 멤버 참조로부터 직접 속성 값을 가져오려면, 멤버 참조(Person::age)를 괄호로 둘러싸야 합니다.

class Person(val name: String, var age: Int)

fun main() {
    val kim = Person("Kim", 23)
    
    val nameReference = Person::name
    println(nameReference.get(kim)) // getter 사용 - Kim
    println(nameReference(kim))     // getter 생략 - Kim
    
    println((Person::age)(kim))     // 23
    println((Person::age).get(kim)) // 23    
}

클래스 Person의 인스턴스 kim을 생성한 다음, "인스턴스_이름::속성_이름"처럼 참조할 수 있습니다.

fun main() {
    val kim = Person("Kim", 23)
    val nameReference = kim::name  // 멤버 참조 - 속성 name 

    println(nameReference)         // Kim
    println(kim::age)              // 멤버 참조 - 속성 age
}

 

■ 클래스 멤버 참조: 메소드
클래스의 멤버 중 메소드는 "클래스_이름::메소드"처럼 참조할 수 있습니다.

class MathOp {
    fun square(x: Int): Int = x * x
}

fun main() {
    val m = MathOp()
    val referencingFunc: MathOp.(Int) -> Int = MathOp::square
    println(m.referencingFunc(5))   // 25
}

재미있는 점은 클래스의 메소드 멤버 참조의 타입과 람다 식의 타입은 같습니다. 따라서 멤버 참조 대신 같은 기능을 하는 람다 식으로 바꿀 수 있습니다.

val referencingFunc: MathOp.(Int) -> Int = MathOp::square
val referencingFunc: MathOp.(Int) -> Int = { x: Int -> x*x }

그러나, 클래스 MathOp의 인스턴스 m을 생성한 다음, "인스턴스_이름::메소드_이름"처럼 참조할 때는 함수 타입은 리플렉션 타입입니다.

fun main() {
    val m = MathOp()
    val referencingFunc: KFunction1<Int, Int> = m::square
    println(referencingFunc(5))   // 25
}

 

■ 멤버 참조: 확장 함수
확장 함수도 클래스 멤버 참조와 같은 방식(Student::isScholarship)으로 참조할 수 있습니다

class Student(val name: String)
fun Student.isScholarship(score: Int): Boolean = score > 90

fun main() {
    val status: Student.(Int) -> Boolean = Student::isScholarship
    val kim = Student("Kim")
    println(kim.status(97))   // true
}

 

■ 클래스 멤버 참조: 생성자
클래스의 인스턴스 생성을 지연시키기 위해 클래스 생성자를 참조할 수 있습니다. 생성자를 참조하려면 "::클래스_이름"을 사용합니다. 

class Person(val name: String, var age: Int)

fun main() {
    val newOne = ::Person
    val park = newOne("Park", 24)
    println("name=${park.name}, age=${park.age}") // name=Park, age=24
}

 

■ 마무리 예제(wrap-up exercise)
멤버 참조를 마무리하면서 4.13 data 클래스에서 소개한 과일 리스트(fruitList)에 적용해 볼까요. 가장 칼로리가 높은 과일은 무엇일까요? 칼로리가 35 미만인 과일은 무엇일까요? 이 질의에 답하는 코드를 멤버 참조를 사용해 작성해 볼까요.

data class Fruit(var name: String, var sciName:String, var calories:Int)

fun main() {
    val fruitList = listOf<Fruit>(
        Fruit("Apple", "Malus domestica", 48),
        . . . . . .
        Fruit("Watermelon", "Citrullus lanatus ", 30))

    val maxCalroiesFruit = fruitList.maxBy ( Fruit::calories )
    // val maxCalroiesFruit = fruitList.maxBy { it.calories }
    println(maxCalroiesFruit::name)   // Banana
    // println(maxCalroiesFruit.name)   
    
    println(fruitList.filter { it.calories < 35 }.map (Fruit::name))
    // println(fruitList.filter { it.calories < 35 }.map { it.name })
    // [Strawberry, Watermelon]
}