Kotlin - start!

함수

kotlin 의 기본적인 함수정의는 아래와 같다.

한줄로 끝날경우 중괄호와 반환타입 모두 생략하고 표현식으로만 함수정의를 해도 된다.

fun add(a: Int, b: Int): Int {
    return a + b
}

fun add2(a: Int, b: Int) = a + b

kotlin 에선 기본 인자, 명시적 인자 문법을 제공한다.

전체인자가 아닌 일부 인자만 명시할 경우 순서를 지켜야 컴파일 오류가 발생하지 않는다.

fun greet(name: String, msg: String = "hello"): String = "$msg $name"

fun main() {
    // 평범한 함수 사용
    println(greet("kouzie", "hi")) // hi kouzie
    // 기본 인자 사용
    println(greet("world")) // hello world
    // 명시적 인자
    println(greet(msg = "is good",  name = "haha")) // is good haha
}

람다함수

java 에서 람다는 함수형 인터페이스를 구현하는 형식으로 아래의 lambda 를 정의한다.

// java 의 lambda
BiFunction<Integer, String, String> lambda = (Integer age, String name) -> {
    String result = "my name is " + name + "and my age is " + age;
    return result;
};

kotlin 에서는 아래처럼 입력Type, 반환Type 을 미리 작성하여 추론없이 바로 사용하거나
입력Type 을 정의하되 반환값은 자동으로 추론되도록 하는 방법이 있다.

// kotiln 의 lambda
val funName: (입력Type) -> (반환Type) = {arg -> body}   // 1 번
val funName = {arg: 입력Type -> body}                  // 2 번

lambda1 함수는 입력Type, 반환Type 모두 정의
lambda2입력Type 만 정의하고 반환Type은 추론되도록 설정

val lambda1: (Int, String) -> (String) = { age, name -> "my name is $name and my age is $age" } // 1 번
val lambda2 = { age: Int, name: String -> "my name is $name and my age is $age" } // 2 번

fun main() {
    println(lambda1(10, "hello")) 
    println(lambda2(15, "world")) 
    // my name is hello and my age is 10
    // my name is world and my age is 15
}

개인적으로 2번 방법이 더 깔끔하고 가독성이 좋아보인다.
람다함수는 한줄로 정의되는 경우가 많기에 반환Type 생략이 더 깔끔해 보임

람다를 파라미터로 받는 일급함수를 정의할 땐 아래와 같이 작성한다.

// 람다를 파라미터로 받음
fun callLambda(lambda: (String) -> Int): Int {
    return lambda("hello world")
}

fun main() {
    // 람다 변수 정의
    val lambda = { str: String -> str.length }
    var result1 = callLambda(lambda)
    println(result1) // 11

    // 리터럴 람다 형식, 매개변수가 람다함수 1개일 경우 사용 가능  
    val result2 = callLambda { it.length }
    println(result2) // 11
}

일반적으로 람다를 매개변수로 받는 함수들은 추가 매개변수가 없어서 리터럴 람다 형식을 사용할 수 있다.
주로 스트림 관련 메서드들이 람다를 파라미터로 받는다.

맨 마지막 매개변수가 람다라면 아래와 같이 리터럴 람다 형식을 사용할 수 있다.
또한 람다의 입력 매개변수가 하나뿐이라면 it 키워드를 사용할 수 있다.

fun walk1To(n: Int, action: (Int) -> Unit) = (1..n).forEach { action(it) }

fun main() {
    walk1To(9) { print(it) } // 123456789
}

operator 키워드 - 연산자 오버로딩

참고 https://kotlinlang.org/docs/operator-overloading.html

operator 키워드로 메서드를 생성하면 kotlin 에서 제공하는 연산자들에 대한 처리를 메서드로 오버로딩할 수 있다.

연산자 대응하는 메소드 주의사항
+x x.unaryPlus()  
-x x.unaryMinus()  
!x x.not()  
x+y x.plus(y)  
x-y x.minus(y)  
x*y x.times(y)  
x / y x.div(y)  
x % y x.rem(y)  
++x x.inc() x 는 할당 가능해야 함
x++ x.inc() x 는 할당 가능해야 함
--x x.dec() x 는 할당 가능해야 함
x-- x.dec() x 는 할당 가능해야 함
x == y x.equals(y)  
x != y !(x.equals(y))  
x < y x.compareTo(y) <=, >, >= 도 사용 가능
x[i] x.get(i)  
x[i] = y x.set(i, y)  
y in x x.contains(y)  
x..y x.rangeTo(y) !in으로도 사용 가능
x() x.invoke()  
x(y) x.invoke(y)  

operator 키워드를 사용해 연산자 오버로딩을 진행한 몇가지 예제.

class Point(var x: Int,
            var y: Int) {
    operator fun plus(p: Point) = Point(x + p.x, y + p.y)

    override fun toString(): String {
        return "($x, $y)"
    }

    operator fun not(): Boolean {
        // Point 객체가 원하는 조건을 만족하는지 여부를 판단
        return x == 0 && y == 0
    }
}

operator fun Point.minus(p: Point) = Point(x - p.x, y - p.y)

fun main() {
    val p1 = Point(1, 2)
    val p2 = Point(3, 4)
    var p3 = p1 + p2
    val p4 = p2 - p1
    println(p1) // (1, 2)
    println(p2) // (3, 4)
    println(p3) // (4, 6)
    println(p4) // (2, 2)
    println(!p4) // false
}

Point.minus 처럼 확장함수로도 정의 가능하기 때문에 kotlin 기본 클래스에 대해서도 operator 키워드 사용이 가능하다.

operator fun Pair<Int, Int>.plus(p: Pair<Int, Int>) = 
    Pair(first + p.first, second + p.second)

fun main() {
    val p1 = Pair(1, 2)
    val p2 = Pair(3, 4)
    val p3 = p1 + p2
    println(p1) // (1, 2)
    println(p2) // (3, 4)
    println(p3) // (4, 6)
}

infix 키워드

대부분의 기능은 operator 키워드를 통해 대부분 연산자 오버로딩이 가능하지만
특수한 기호를 연산자로 사용하고 싶을 경우 infix 키워드를 사용해 변수간 중위표기 방식을 구현할 수 있다.

pow 연산자로 누승을 하는 infix 함수 정의

infix fun Int.pow(i: Int): Int {
    return toDouble().pow(i).toInt()
}

fun main() {
    var a: Int = 2 pow 2;
    println(a) // 4
}

infix 함수로 일반 문자열을 사용할땐 백탭을 쓰지 않아도 되지만 특수문자를 사용할땐 백탭으로 감싼다.

infix fun Int.`^`(i: Int): Int {
    return toDouble().pow(i).toInt()
}

fun main() {
    var a: Int = 2 `^` 2;
    println(a) // 4
}

대표적인 infix 키워드로 제공되는 함수는 to 함수

val p1 = Pair("1", 1)
val p2 = "1" to 1
// public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

inline 키워드 - 고차함수

https://codechacha.com/ko/kotlin-inline-functions/

인자값으로 함수를 전달받거나 반환하는 함수를 고차함수라 한다.
이 고차함수를 효율적으로 사용하기 위한 키워드가 inline 이다.

고차함수 내부에서 전달받은 람다함수를 구현하는 클래스를 자동생성하게 된다.
이때 추가적인 메모리 할당 및 함수호출로 Runtime overhead 가 발생하는데

inline 키워드로 함수를 정의할 경우 함수 코드블럭 자체내용을 호출되는 위치에 복사하여
고차함수 호출시 발생하는 오버헤드를 방지한다.

inline fun someMethod(a: Int, func: () -> Unit):Int {
    func()
    return 2 * a
}

fun main(args: Array<String>) {
    var result = someMethod(2, { println("Just some dummy function") })
    println(result)
}

위와 같이 고차함수 를 매개변수로 받는 함수에다 inline 키워드를 사용하면

컴파일 처리 과정에서 메인함수의 코드는 아래와 같이 변하게 된다.

// 컴파일 뒤 변한 jvm 코드
@JvmStatic
public static final void main(@NotNull String[] args) {
    int a = 2;
    int var5 = false;
    String var6 = "Just some dummy function";
    System.out.println(var6);
    int result = 2 * a;
    System.out.println(result);
}
...

inline 함수의 코드부분이 그대로 이동하게 되며 추가적인 클래스 생성을 방지한다.

하지만, inline functions 는 내부적으로 코드를 복사하기 때문에,
inline 함수에서 인자로 전달받은 고차함수를 변수화 해서 다시 다른 함수로 전달하거나 참조시킬 수 없다.

코드 양이 많은 함수를 inline 처리하면 되려 byte code 가 더 많아질 수 있음으로
inline keyword는 1~3줄 정도 길이의 함수에 사용하는 것이 효과적이다.

reified

구체화된 타입 파라미터(Reified type Parameter)

inline 키워드 사용시 reified 키워드 사용이 가능하다.

java 의 경우 특정 타입을 검증하는 코드를 작성할 때, 해당 타입을 변수로 빼거나 파라미터로 넘기는 경우가 많다.
kotlin 에서도 아래와 같이 class 타입을 넘겨 검증하는 코드를 작성할 수 있다.

class Banana(val num: Int) : Fruit {
    override fun toString() = "$num banana"
}

class Apple(val num: Int) : Fruit {
    override fun toString() = "$num apple"
}

// class 타입 검증
fun <T> findFirst(fruits: List<Fruit>, ofClass: Class<T>): T {
    var result: Fruit = fruits.first { fruit -> ofClass.isInstance(fruit) }
    return result as T
}

fun main() {
    val fruits = listOf(Apple(1), Apple(2), Banana(3))
    val firstBanana: Banana = findFirst(fruits, Banana::class.java)
    println(firstBanana) // 3 banana
}

inline 함수와 reified 키워드를 사용하면 굳이 파라미터로 클래스타입을 전달할 필요가 없다.

interface Fruit

class Banana(val num: Int) : Fruit {
    override fun toString() = "$num banana"
}

class Apple(val num: Int) : Fruit {
    override fun toString() = "$num apple"
}

inline fun <reified T> findFirst(fruits: List<Fruit>): T {
    var result: Fruit = fruits.first { fruit -> fruit is T }
    return result as T;
}

fun main() {
    val fruits = listOf(Apple(1), Apple(2), Banana(3))
    val firstBanana: Banana = findFirst<Banana>(fruits)
    println(firstBanana) // 3 banana
}

noinline

매개변수로 전달받은 함수인자 앞에 noinline 키워드를 붙이면 해당 인자는 inline 조건에서 제외된다 noinline 키워드가 붙은 함수인자는 다른 함수의 인자로 전달하는 것이 가능해진다.

inline fun newMethod(a: Int, func: () -> Unit, noinline func2: () -> Unit) {
    func()
    someMethod(10, func2)
}

fun someMethod(a: Int, func: () -> Unit):Int {
    func()
    return 2*a
}

label return, non-local return

kotlin 에선 람다함수에 return 키워드만을 사용해 함수 블럭을 빠져나가는 것을 지원하지 않는다.
아래처럼 label return을 통해 함수 블럭을 빠져나갈 수 있다.

라벨은 함수블럭 앞에 지정하는 것을 명시적 label return 이라 부른다.
블럭앞에 별도의 라벨을 지정하지 않고 함수명을 사용해도 된다.

fun walk1To(n: Int, action: (Int) -> Unit) = (1..n).forEach { action(it) }

fun test1() {
    walk1To(9) here@{ i: Int ->
        if (i == 5) return@here
        else print(i)
    }
    // 12346789
}

fun test2() {
    walk1To(9) { i: Int ->
        if (i == 5) return@walk1To
        else print(i)
    }
}

예외적으로 inline 으로 정의된 함수에선 return 키워드를 통해 람다 함수를 빠져나올 수 있다.

inline fun walk1To(n: Int, action: (Int) -> Unit) = (1..n).forEach { action(it) }

fun main() {
    walk1To(9) { i: Int ->
        if (i == 5) return
        else print(i)
    }
    println("main function finished") // 출력되지 않음
}

inline 특성상 main 메서드에 wlak1To 메서드의 코드가 붙여넣어지기 때문에 실제로 return 하는 것은 main 메서드이다.
함수를 빠져나오는 것을 non-local return 이라 부른다.

forEach 메서드의 경우 inline 으로 정의되어 있어 non-local return 사용이 가능하다.

fun main() {
    (1..9).forEach { i ->
        if (i == 5) return
        else print(i)
    }

    println("main function finished") // 출력되지 않음
}

오히려 non-local return 을 피하기 위해 label return 을 사용해야 하는 상황도 생길 수 있다.

Function literals with receiver

일반적으로 (B) -> C 형식의 람다함수 타입이 익숙하다.

kotlin 에선 Function literals with receiver 를 제공한다.

A.(B) -> C 형식의 함수 타입, receiver 타입을 가지는 특이한 함수

Areceiver 라 하며 . 앞에 명시, 함수 호출을 위한 사전타입이라 할 수 있다.
B 는 매개변수, C 는 반환타입이다.

fun main() {
    val sum: Long.(Int) -> String = { other -> (this + other).toString() }
    val result: String = 1L.sum(2)
    println(result) // 3
}

sum 의 함수호출이 특히한것을 알 수 있다.
Long 타입을 receiver 삼아 sum 함수를 호출한다.

vararg, 스프레드 연산자

입력된 인자값을 기준으로 배열을 생성하는 intArrayOf 메서드 내부를 보면 vararg 키워드를 사용해 여러개의 인자를 전달받는다.

전달받은 인자는 Array 로 취급되어 사용된다.

fun max(vararg elem: Int): Int {
    if (elem.isEmpty())
        throw IllegalArgumentException("max argument is null")
    var max = elem[0]
    for (i in 1 until elem.size) {
        if (elem[i] > max) max = elem[i]
    }
    return max
}

아래와 같이 여러개의 인자를 나열해서 사용하거나, 배열과 스프레드 연산자(*) 를 사용할 수 있다.

fun main() {
    println(max(1, 6, 2, 3, 4, 5))
    val arr = intArrayOf(1, 2, 3, 4, 5)
    println(max(*arr))
}

만약 Array 대신 List 형식의 데이터를 사용한다면 아래와 같이 스프레드 연산자와 toArray 메서드를 같이 사용하면 된다.

val arr = listOf(1, 2, 3, 4, 5)
println(max(*arr.toIntArray()))

탑레벨 함수

kotlin 에선 함수, 변수를 클래스 안에 꼭 포함시킬 필요가 없다.
아래와 같이 파일 탑레벨에 함수를 정의하고 타 파일에서 바로 호출가능하다.

// topLevel.kt
// 탑레벨 함수
fun unitsSupported() = listOf("Metric", "Imperial")
fun precision(): Int = throw RuntimeException("Not implemented yet")
// main.kt
import com.example.study.blog.util.unitsSupported

fun main() {
    unitsSupported();
}

컴파일된 바이트코드에선 자동 생성된 클래스의 static 메서드로 관리된다.

스코프와 mutable

kotlin 에선 lexical scope 를 사용하며 정의된 함수 외부에 정의되어 있는 변수를 참조 가능하다.

const val greetMsg: String = "Hello World!"

fun greet() {
    println(greetMsg)
}

fun main() {
    greet(); // Hello World!
}

만약 참조하는 외부변수가 mutable 변수라면 사용하는 함수에 따라 문제가 생길 수 있다.

var factor: Int = 2
val doubled: List<Int> = listOf(1, 2).map { it * factor }
val doubledAlso: Sequence<Int> = sequenceOf(1, 2).map { it * factor }
factor = 3
doubled.forEach { print("$it,") } // 2,4,
doubledAlso.forEach { print("$it,") } // 3,6,

이런 차이가 발생하는 이유는 Sequence 가 지연연산을 지원하지 때문이다.

List 의 경우 선언과 실행이 한 라인 안에서 같이 실행되지만 Sequence 의 경우 실제 해당 요소를 참조하는 시점에서 실행된다.

둘다 외부변수 factor 참조하지만 출력값이 다른것을 알 수 있다.

개발자가 각 함수별 실행시점을 모두 알고있을순 없음으로 외부변수를 참조할 땐 최대한 immutable 를 사용하는 것을 권장하며
함수형 프로그래밍을 지향하는 요즘 외부변수를 참조를 하지 않는것을 권장한다.

tailrec

tailrec 키워드는 재귀호출 최적화시 사용하는 키워드로, 재귀호출을 반복문을 사용하는 프로시저로 변경하여 스택 오버플로우 오류를 방지한다.

주의할점은 아래와 같이 단일라인 연산이어야 한다.

tailrec fun factorial(n: Int, result: BigInteger = 1.toBigInteger()): BigInteger =
        if (n <= 0) 
            result
        else 
            factorial(n - 1, result * n.toBigInteger())

fun main() {
    println(factorialRec(50000))
}

카테고리:

업데이트: