kotlin - 람다와 멤버참조

조갱·2022년 6월 12일
0

kotlin

목록 보기
5/10

요즘 kotlin in action 책을 읽고 있다. (이전 포스팅 - kotlin 스럽다는 것)
입사할 때 숙제 (?)로 한번 읽었지만, 아무래도 처음 접해본 kotlin이었기 때문에 그 당시에 '무슨 말이지..?' 싶었다면, 이제는 조금씩 이해가 되면서 읽히는게 느껴진다.

또한, 코틀린스럽게 작성하는 방법을 익히기 위해 (알고리즘 같은) 문제를 풀고 있는데, 모범답안을 보면서 궁금했던 부분들이 많이 있었다. 오늘은 이러한 궁금증에 대해 해소하는 시간을 가져본다.

그래서 궁금했던 점은?

val maxAge = employee.maxByOrNull(Person::age) ?: 0
val maxAge = employee.maxByOrNull { it.age } ?: 0

은 왜 같을까?

람다 (lambda)

간단하게, 람다는 익명함수이다.
주로 메소드를 따로 선언하지 않고 일회용으로만 사용할 때 사용한다.
(람다함수를 통해 메소드를 선언할 수도 있다.)

예를 들어, 두 개의 정수 입력받아 합을 출력하는 sum (Int, Int) 메소드를 람다식으로 만들어보자.

val sum = { x: Int, y: Int -> x + y }

>>> print(sum(2, 3))
>>> 5

기본 문법

람다의 기본 문법은 다음과 같다.
{ x: Int, y: Int -> x + y }

kotlin에서 람다식은 항상 중괄호 사이에 표현한다.
x: Int, y: Int : 파라미터
-> : 파라미터와 본문을 나눈다.
x + y : 본문

본문은 여러줄일 수 있고, 본문의 마지막 줄이 반환값이 된다.
즉, 위에 sum은 아래와 같이 쓸 수도 있다.

val sum = { x: Int, y: Int ->
	println("your Input >> x: ${x}, y: ${y}") // 그냥 출력만 한다. 반환값에는 영향 없음
    x + y // 람다의 마지막 줄인 x + y가 반환된다.
}

>>> println(sum(2, 3))
>>> your Input >> x: 2, y: 3
>>> 5

kotlin의 컬렉션 라이브러리 함수

kotlin에서는 Collection Interface에서 제공하는 몇몇 라이브러리 함수가 있다.
그 중에서 maxByOrNull 은 컬렉션에서 최댓값을 찾아주는 역할을 한다.
(메소드 명에서도 볼 수 있듯, 원소가 없으면 Null을 반환)

우선 함수의 프로토타입과 구현부를 보자.

public inline fun <T, R : Comparable<R>> Iterable<T>.maxByOrNull(selector: (T) -> R): T? {
    val iterator = iterator()
    if (!iterator.hasNext()) return null
    var maxElem = iterator.next()
    if (!iterator.hasNext()) return maxElem
    var maxValue = selector(maxElem)
    do {
        val e = iterator.next()
        val v = selector(e)
        if (maxValue < v) {
            maxElem = e
            maxValue = v
        }
    } while (iterator.hasNext())
    return maxElem
}

오늘 얘기하고자 하는 내용중에서, 가장 중요한 부분은 파라미터 (selector: (T) -> R)이다. 추후에 다시 등장할 예정이니 꼭 기억해두자.

위에서 살펴본 maxByOrNull 함수를 보면 아래 코드에서 employee중 나이가 제일 많은 사람은 아래와 같이 찾을 수 있겠다.

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

val employee = listOf(Person("홍길동", 27),
					  Person("김철수", 30),
                      Person("김영희", 33))
                      
// Person 객체를 받아 age변수를 반환한다.
// 예를 들어, getPersonAge(employee[0]) -> 27 반환
val getPersonAge = { person: Person -> person.age } 
>>> print(employee.maxByOrNull(getPersonAge())) 
>>> 33

위에서 getPersonAge는 maxByOrNull() 메소드에 1회만 쓰이니, 람다식으로 사용해보자.

// 상단 생략...
>>> print(employee.maxByOrNull({ person: Person -> person.age })) 
>>> 33

코틀린에서는 함수의 파라미터 목록 중 마지막이 람다인 경우는 뒤로 뺄 수 있다.

>>> print(employee.maxByOrNull() { person: Person -> person.age })
>>> 33

코틀린에서는 람다가 어떤 함수의 유일한 인자이고, 괄호 뒤에 람다를 썼다면 호출 시 빈 괄호를 없애도 된다.

>>> print(employee.maxByOrNull { person: Person -> person.age})
>>> 33

컴파일러가 유추할 수 있는 타입은 생략할 수 있다.
employee는 Person 타입이므로 : Person 과 같은 타입 지정은 뺄 수 있다.

유추할 수 없는 타입은 어떤게 있을까? 일반적으로는 상속 관계를 생각해볼 수 있겠다.
Employee -> Person, Employer -> Person 과 같이 Person이 부모 클래스이고, 이를 상속받는 Employee, Employer 클래스가 있다고 가정하자.
list가 Person 타입이면
personList.maxByOrNull { employee: Employee -> employee.age }
와 같이 자식 타입을 명시적으로 지정할 수 있다.

>>> print(employee.maxByOrNull { person -> person.age })
>>> 33

람다의 파라미터가 1개 뿐이고 (위 예제에서는 person), 그 타입을 파라미터가 추론할 수 있으면 it (디폴트 파라미터)를 사용할 수 있다.

>>> print(employee.maxByOrNull { it.age })
>>> 33

이제 list에서 최대 나이를 구하는 코드가 조금 리팩토링 되었다.

멤버 참조 (member reference)

kotlin에는 멤버 참조라는 개념이 존재한다.
멤버 참조는 kotlin의 리플렉션 중 하나이며, 이를 kotlin의 일급 객체를 얻을 수 있다.
(일반적으로 리플렉션은 Json Deserializer 에서 주로 사용된다.)

멤버 참조
::를 사용하는 식으로, 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다.
리플렉션 (Reflection)
런타임 중 프로그램의 구조를 조사할 수 있게 해주는 언어 및 라이브러리 기능 세트

아까 만들었던 Person 클래스를 다시 보자.

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

여기서 멤버참조를 사용할 수 있는데, Person::name 은 Person의 name 프로퍼티를 일급 객체로 반환한다. 그래서 Person::name은 KProperty1<T, out V> 타입을 가지고 있다.

여기서 T는 Person 의 인스턴스를 받아, String (name)을 반환한다.
즉, (Person) -> String 을 구현한다.

참고로,
person = Person("김영희", 20)
person::age // 인스턴스에서 일급 객체 : KProperty0<Int>를 구현
Person::age // 클래스에서 일급 객체 : KProperty1<Person, Int>를 구현

어디서 많이 본 내용이다.

아까 설명했던 maxByOrNull의 프로토타입을 다시 가져온다.

public inline fun <T, R : Comparable<R>> Iterable<T>.maxByOrNull(selector: (T) -> R): T?

여기서 아까 중요했던 파라미터 (selector: (T) -> R)을 보면, 위에서 언급한 멤버참조 (Person::age) 이 구현하는 (Person) -> String과 일치하다.

따라서 위 예제에서

>>> print(employee.maxByOrNull { it.age })
>>> 33
>>> print(employee.maxByOrNull(Person::age))
>>> 33

가 동일해지는 것이다.

정리
메소드, 생성자, 프로퍼티의 이름 앞에 ::을 붙임녀 각각에 대한 참조를 만들 수 있다. 그런 참조를 람다 대신 다른 함수에 넘길 수 있다.

번외: 헷갈릴 수 있는 4가지 케이스

출처 : https://stackoverflow.com/questions/59822713/member-reference-in-kotlin

fun main(args: Array<String>) {
    val list = arrayListOf<Student>(Student(200, "Lim"), Student(100, "Kim"), Student(300, "Park"))

    // map and print field reference of the class
    println(list.map { Student::name })

    // map and print field reference for each instance by lambda expression
    println(list.map { student -> student::name })

    // map and print values by field reference for each instance
    println(list.map(Student::name))

    // map and print values by transformation for each instance by lambda expression
    println(list.map { student -> student.name })
}

>>> [val de.os.kotlin.Student.name: kotlin.String, val de.os.kotlin.Student.name: kotlin.String, val de.os.kotlin.Student.name: kotlin.String]
>>> [val de.os.kotlin.Student.name: kotlin.String, val de.os.kotlin.Student.name: kotlin.String, val de.os.kotlin.Student.name: kotlin.String]
>>> [Lim, Kim, Park]
>>> [Lim, Kim, Park]

Reference
멤버 참조 : https://stackoverflow.com/questions/59822713/member-reference-in-kotlin
kotlin Reflection: https://kotlinlang.org/docs/reflection.html#property-references

profile
A fast learner.

0개의 댓글