kotlin

코틀린 이모저모 - 5

e4g3r 2025. 4. 23. 18:42

Function Parameter

fun param(a: Int, b: Int = 100, c: Int = 100): Int {
    return a + b + c
}

fun main() {
    param(a = 1, b = 2, c = 3)
    param(a = 1, c = 3)
}

 

코틀린에서 함수의 파라미터는 default 값을 가질 수 있습니다. 위 코드에서도 param 함수를 호출할 때 default 값이 있는 파라미터를

전달하지 않는 경우는 default 값이 사용되며 함수를 호출할 때 파라미터 이름을 명시해서 어떤 값을 전달할 지 정할수도 있습니다.

Single-expression function

fun double(x: Int): Int = x * 2

fun double(x: Int) = x * 2

 

코틀린에서 함수의 본문이 하나의 행동만 한다면 {}로 묶을 필요없이 위 코드처럼 단일 표현 함수로 선언할 수 있습니다.

또한 단일 표현 함수의 경우 컴파일러가 반환되는 타입을 유추할 수 있기에 함수 시그니처에 반환 타입의 생략도 가능합니다.

Infix notation

infix fun Int.shl(x: Int): Int { ... }

// calling the function using the infix notation
1 shl 2

// is the same as
1.shl(2)

 

코틀린에서 infix 키워드가 사용된 확장 함수를 사용할 때에는 .을 사용하지 않고 함수를 호출할 수 있습니다.

1 shl 2는 1.shl(2)를 호출한 것과 동일합니다.

class IntStorage(
    var value: Int,
) {
    infix fun add(addValue: Int) {
        value += addValue
    }
}

fun main() {
    val storage = IntStorage(100)

    storage.add(1)

    storage add 1
}

 

infix 키워드는 확장 함수이외에도 클래스 멤버 함수에서 사용이 가능합니다.

Local Function

fun outerFun() {
    var outerValue = 100

    fun innerFun() {
        outerValue += 1
    }

    innerFun()
}

 

코틀린은 함수 내부에서 함수를 선언할 수 있습니다. 위 코드에서 innerFun 함수는 내부에 선언된 지역 함수로 지역 함수는 outer에

존재하는 변수에 접근이 가능합니다.

First-class Function

fun method() {}

fun main() {
    val methodRef = ::method
    methodRef()
}

 

코틀린에서 함수는 일급 객체로 취급됩니다. 일급 객체라 함은 함수를 변수에 저장할 수 있다는 의미와 동일합니다.

위 코드에서는 :: 연산자를 통해 method 함수의 참조를 methodRef에 저장하였고 methodRef()로 method를 호출할 수 있습니다.

Higher-order function

코틀린에서 함수는 일급 객체 즉 변수에 저장이 가능하기 때문에 함수의 파라미터에 함수가 전달될 수 있습니다.

또한 함수가 어떠한 함수를 반환할 수도 있습니다. 이처럼 파라미터로 함수를 받거나, 함수를 반환하는 함수를 고차 함수라고 표현합니다.

fun main() {
    val strList = listOf("A", "B", "C", "D", "E")
    strList.forEach {
        println(it)
    }
}

 

list의 forEach 함수는 아이템을 이용해서 수행할 로직, 즉 함수를 전달받습니다.

@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

 

forEach의 구현을 보면 파라미터로 함수를 전달받습니다. 그리고 아이템을 순회하면서 전달받은 함수를 파라미터로 담아 호출합니다.

결국 함수를 파라미터로 받는 forEach는 고차함수라고 볼 수 있습니다.

Function type

fun method1(func: () -> Unit) {}

fun method2(func: (String) -> Unit) {}

fun method3(func: (String, Int) -> Unit) {}

fun method4(func: (String, Int) -> String) {}

fun method5(func: (a: String, b: Int, c: String, d: Int) -> String) {}

 

함수를 파라미터로 받기 때문에 함수의 타입도 존재합니다. 함수 타입은 (파라미터 타입) -> (반환 타입) 으로 구성됩니다.

Instantiating a function type

코틀린 공식문서에서는 함수의 타입에 맞게 함수를 인스턴스화 하는 방법 몇가지를 소개합니다.

fun main() {
    val func1: (Int) -> Int = { value -> value + 1 }

    val func2: (Int) -> Int = fun(value: Int): Int {
        return value + 1
    }
}

 

위 코드에서는 Int 타입 파라미터 1개를 받아 Int 타입을 반환하는 함수 타입을 람다, 익명 함수를 통해 인스턴스화 하였습니다. 

fun alreadyExistsFunc(value: Int): Int {
    return value + 1;
}

fun main() {
    val func1: (Int) -> Int = ::alreadyExistsFunc
}

 

함수 타입과 동일한 시그니처를 가진 기존 함수가 존재한다면 해당 함수를 :: 연산자를 통해 참조하여 인스턴스화 할 수 있습니다.

class FuncImplement : (Int) -> Int {
    override operator fun invoke(value: Int): Int {
        return value + 1
    }
}

fun main() {
    val func1: (Int) -> Int = FuncImplement()
}

 

또한 함수 타입을 구현하는 별도의 구현 클래스를 정의하고 invoke 메서드를 오버라이딩하는 방식을 통해 인스턴스화 할 수 있습니다.

class FuncImplement : (Int) -> Int {
    override operator fun invoke(value: Int): Int {
        return value + 1
    }
}

fun main() {
    val func1: (Int) -> Int = FuncImplement()
    func1(1)
    func1.invoke(1)
}

 

Function Type의 경우 invoke 연산자를 가지고 있으며 invoke를 호출하는 것은 함수를 호출하는 것과 동일합니다.

Lambda

fun runMethod(func: () -> Unit) {
    func()
}

fun main() {
    runMethod(
        {
            println()
        }
    )

    runMethod {
        println()
    }
}

 

람다식을 함수 파라미터로 전달할 때 마지막 파라미터인경우 괄호 밖 {} 블럭을 통해 전달할 수 있습니다.

fun plusOne(list: List<Int>, func: (Int) -> Int) {
    for (item in list) {
        func(item)
    }
}

fun main() {
    val list: List<Int> = listOf(1, 2, 3, 4, 5, 6)
    plusOne(list) { it * it }
    plusOne(list) { value: Int -> value * value }
}

 

plusOne에서 마지막 파라미터는 함수입니다. main에서 plusOne을 호출할 때 list와 람다식을 전달합니다.

람다식은 1개의 Int 파라미터와 Int 타입을 반환합니다. 이처럼 람다식의 파라미터가 1개인 경우 it이라는 키워드를 사용할 수 있습니다.

(value: Int -> value * value)는 it * it로 표현될 수 있습니다. 타입 명시->가 생략 됩니다.

Lambda / Anonymous function Closures

fun plusOne(func: () -> Unit) {
    func()
}

fun main() {
    var value: Int = 100
    plusOne { value += 1 }
    plusOne(fun() {
        value += 1
    })

    println(value)
}

 

람다, 익명함수 모두 외부 변수에 접근할 수 있습니다. 따라서 plusOne에서 전달받은 함수를 실행하면 main의 value 값이 변경됩니다.

Function literals with receiver

val printTwice1: String.() -> Unit = {
    println(this)
    println(this)
}

val printTwice2 = fun String.(): Unit {
    println(this)
    println(this)
}

fun main() {
    val str = "abcd"
    str.printTwice1()
    str.printTwice2()
}

 

함수 타입도 수신자가 존재할 수 있습니다.  위 코드 printTwice의 함수 타입은 파라미터 및 반환값이 없습니다.

그러나 파라미터 앞에 String.으로 수신자를 정의했습니다. 수신자는 String 객체이므로 확장 함수와 비슷하게 String 타입의 객체에서

printTwice 메서드를 호출할 수 있게 됩니다.

fun main() {
    val str = "abcd"
    printTwice1(str)
    printTwice2(str)
}

 

String.printTwice() 형태로 호출이 가능하지만 printTwice(String)의 형태로도 호출이 가능합니다.

중요한 것은 항상 수신자 대상 즉 String 타입의 객체가 필요로 하다는 점입니다.

Inline Function

fun runMethod(func: () -> Unit) {
    func()
}

fun main() {
    var value = 0
    runMethod {
        value += 1
    }
}

 

위 runMethod는 파라미터로 함수를 받는 고차함수입니다. 고차함수는 위에서 언급된 Closure 처리하는 과정에서 오버헤드가

발생할 수 있다고 공식문서에서 언급합니다. 또한 파라미터로 전달되는 함수들의 경우 함수 타입의 구현체이기에 

내부 메서드 invoke를 통해 실질적으로 호출되는 가상 호출 오버헤드가 발생한다고도 언급합니다.

inline fun runMethod(func: () -> Unit) {
    func()
}

fun main() {
    var value = 0
    runMethod {
        value += 1
    }
}

// --------------------------- 

fun main() {
    var value = 0
    value += 1
}

 

이러한 고차함수를 선언할 때 inline 키워드를 사용할 수 있습니다.

inline 키워드의 설명을 보면 파라미터로 전달된 함수가 실제 삽입된 것 처럼 컴파일 과정에서 빌드된다고 설명합니다.

따라서 기존 main 함수에서 runMethod에 람다를 전달하고 호출하는 과정은 사라지고 실제 람다 본문의 내용으로 변환됩니다.

 

inline 함수의 경우 생성된 코드의 크기가 증가되기 때문에 이점이 있는지, 성능 향상이 되는지 판단하고 사용해야할 것 같습니다.

Reified type

fun <T> List<Any>.filterByType(target: Class<T>): List<T> {
    val result = mutableListOf<T>()
    for (element in this) {
        if (target.isInstance(element)) {
            result.add(target.cast(element))
        }
    }
    return result
}

fun main() {
    val list = listOf(1, 2, 3, "a", "b", "c")
    val filteredList = list.filterByType(String::class.java)
    println(filteredList)
}

 

위 코드는 리스트에서 특정 타입의 요소들만 추출하는 코드입니다. 타겟 타입을 파라미터로 전달하고 리플랙션의 isInstance를 통해

검증 후 캐스팅하여 새로운 리스트를 반환합니다.

inline fun <reified T> List<Any>.filterByType(): List<T> {
    val result = mutableListOf<T>()
    for (element in this) {
        if (element is T) {
            result.add(element)
        }
    }
    return result
}

fun main() {
    val list = listOf(1, 2, 3, "a", "b", "c")
    val filteredList = list.filterByType<String>()
    println(filteredList)
}

// ---------------------------------------------

fun main() {
    val list = listOf(1, 2, 3, "a", "b", "c")
    val result = mutableListOf<String>()
    for (element in list) {
        if (element is String) {
            result.add(element)
        }
    }
    println(result)
}

 

filterByType 함수를 inline으로 선언하고 제네릭 타입 T에 reified를 사용해주면 함수 내부에서 리플렉션 없이 is 연산자로 타입을

검사할 수 있습니다. inline이기 때문에 실제 생성 코드는 T가 String으로 변환되어 타입을 비교하는 것과 동일하게 처리되기 때문입니다.