고차함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수를 말한다.
선언하는 방법은 아래와 같다
val sum1: (Int, Int) -> Int = { x, y -> x + y}
val sum2 = { x: Int, y: Int -> x + y }
sum1의 타입을 보면 (Int, Int) -> Int 로 Int 타입인자 2개를 받고 Int를 반환한다는 뜻으로 해석 할 수 있다.
이러한 반환타입은 코틀린 컴파일러가 타입 추론을 제공하므로 sum2 와 같이 반환값을 생략하고 람다 파라미터의 타입을 명시함으로써 타입추론을 할 수 있게 도와준다.
따라서 함수타입은 아래와 같이 정의된다 할 수 있다.
(파라미터 타입) -> 반환타입
함수 또한 타입이기에 함수에 인자로 사용이 가능하다.
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3)
println("The result is $result")
}
이처럼 2,3에 대한 계산을 하는 함수로 전달받은 함수를 통해 연산을 한 뒤 해당 값을 출력하는 예제이다.
이러한 함수타입을 인자로 받을 경우에도 = 을 이용해서 디폴트 값을 설정할 수 있다.
함수를 인자로 받는 경우가 활용도가 더 높은 경우가 있다.
다만 함수를 반환하는 경우는 예를 들어 배송 수단에 따라 결제 로직이 다를때 수단에 따라 로직을 반환하는 경우이다.
아래 예제를 보면 반환타입으로 함수 타입을 지정한 걸 확인할 수 있다.
fun getShippingCostCalculator(
delivery: Delivery,
): (Order) -> Double {
return when (delivery) {
Delivery.EXPEDITED -> { order -> 6 + 2.1 * order.itemCount }
Delivery.STANDARD -> { order -> 1.2 * order.itemCount }
}
}
함수타입을 이용해 람다를 실행시키면 기본적으로 무명클래스를 생성해서 동작한다. 이렇게 되면 객체 생성의 비용이 들게 되고 이는 성능에 영향이 간다.
이를 개선하는 방법은 람다로 사용할 함수에 Inline 변경자를 붙여주는 거다. 이렇게 Inline 변경자가 붙은 함수는 무명 클래스를 생성하지 않고 호출 되는 함수에 인라인화 되어 부가적인 비용을 생략할 수 있다.
아래 예제를 보면 synchronized() 라는 인라인 함수가 있다.
이 함수를 호출하는 foo() 함수에서는 매개 변수로 있는 함수를 직접 넘겨주어 전체가 인라인화 되지만 LockOwner클래스에 runUnderLock()은 런타임시점에 함수가 정해지므로 body 부분은 무명클래스를 생성하게 된다.
inline fun <T> synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
} finally {
lock.unlock()
}
}
fun foo(l: Lock) {
println("Before sync")
synchronized(l) { println("Action") }
println("After sync")
}
class LockOwner(
val lock: Lock,
) {
fun runUnderLock(body: () -> Unit) {
synchronized(lock, body)
}
}
이렇듯 인라이닝을 사용하면 성능에 대한 걱정을 하지 않아도 된다.
컬렉션에서 기본적으로 제공하는 람다를 인자로 받는 API들은 인라인을 지원하기에 성능상에 문제가 없으니 걱정하지 않고 사용할 수 있다.
여기서 주의할 점은 시퀀스의 경우에는 인라인을 제공하지 않기에 크기가 작은 컬렉션을 시퀀스로 변환해서 사용할 경우 오히려 성능이 안좋을 수도 있다.
따라서 크기에 따라 시퀀스를 쓸지 말지 고민해봐야 한다.
Inline 키워드를 무조건 붙이는 건 좋지 않다. 인라인화 하는 함수의 코드양이 상당히 많다면 호출하는 곳에서 바이트코드가 상당해지기 때문에 이또한 성능상 단점이 될 수도 있다. 따라서 함수의 크기, 용도에 따라서 적절히 사용하는 게 좋다.