[Kotlin] 3. 선택 표현과 처리: enum과 when

2023. 3. 22. 09:20Programming Languages/Kotlin

1. enum 클래스

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
  • Java에서는 enum 키워드를 쓰지만 Kotlin에서는 enum class 키워드로 enum 클래스를 만든다. Kotlin에서 enum은 소프트 키워드(soft keyword)라고 부른다.
  • 소프트 키워드는 특정 상황에서만 키워드로 사용되는 키워드로 그 상황이 아니라면 일반적인 상황에서 사용할 수 있다.
⚠️class는 키워드이다. 따라서 class라는 이름은 사용할 수 없으므로 clazz, aClass와 같이 사용해야 한다.

 

Kotlin의 enum 또한 프로퍼티와 메서드를 정의할 수 있다.

enum class Color (
    val r: Int, val g: Int, val b: Int
) {
    RED(255, 0, 0), ORANGE(255, 165, 0),
    YELLOW(255, 255), GREEN(0, 255, 0), BLUE(0, 0, 255), 
    INDIGO(75, 0, 130), VIOLET(238, 130, 238);

    fun rgb() = (r * 256 + g) * 256 + b
}

println(Color.BLUE.rgb()) // 255
  • 각 enum의 상수를 지정할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야 한다.
⚠️Kotlin에서 유일하게 세미콜론이 필수인 부분이 이 경우이다. enum 클래스에서 메서드를 정의하는 경우 enum 클래스의 상수 목록과 메서드 사이에 반드시 세미콜론을 넣어야 한다.

2. when으로 enum 클래스 다루기

Java의 switch 문은 Kotlin에 when 식으로 사용할 수 있다. 하지만 switch 문보다 훨씬 강력하게 활용할 수 있다.

fun getMnemonic(color: Color) {
    when (color) {
        Color.RED -> "Richard"
        Color.ORNAGE -> "Of"
        Color.YELLOW -> "York"
        Color.GREEN -> "Gave"
        Color.BLUE -> "Battle"
        Color.INDIGO -> "In"
        Color.VIOLET -> "Vain"
    }
}

println(getMnemonic(Color.RED)) // Richard
  • break가 없어도 된다.
  • 한 분기에 여러 값과 매칭할 수 있다. 이 경우 콤마(,)로 구분된다.
fun getWarmth(color: Color) = when(color) {
    Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
    Color.GREEN -> "neutral"
    Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}

println(getWarmth(Color.RED)) // warm

3. when과 임의의 객체를 함께 사용

Java에서 switch는 enum 상수 또는 숫자 리터럴만 사용할 수 있었지만 Kotlin에서는 임의의 객체를 허용한다.

import colors.Color
import colors.Color.*

fun mix(c1: Color, c2: Color) =
    when (setOf(c1, c2)) {
        setOf(RED, YELLOW) -> ORANGE
        setOf(YELLOW, BLUE) -> GREEN
        setOf(BLUE, VIOLET) -> INDIGO
        else -> throw Exception("Dirty color")
    }

println(mix(BLUE, VIOLET) // INDIGO

위 같은 경우 setOf(c1, c2)가 when의 인자로 들어갔으며 각 분기에서는 해당 인자와 동등성(equality)을 검사한다. 즉 위의 when은 c1과 c2가 각 분기 조건에 맞는지를 확인하고 결과를 반환한다. 만약 어떤 조건도 맞지 않으면 else 분기가 실행된다.

4. 인자 없는 when

3의 경우 비교할 때마다 Set의 인스턴스를 생성한다는 점이 걸리는 사람도 있을 것이다. 앞 예제의 경우엔 큰 문제가 되지 않지만 이 함수가 자주 호출된다면 불필요한 가비지 객체가 늘어나기 때문에 함수를 고치는 것이 낫다. 인자가 없는 when 식을 사용하면 읽기는 조금 어려워지지만 성능이 향상된다.

fun mixOptimized(c1: Color, c2: Color) =
    when {
        (c1 == RED && c2 == YELLOW) ||
        (c1 == YELLOW && c2 == RED) ->
            ORANGE

        (c1 == YELLOW && c2 == BLUE) ||
        (c1 == BLUE && c2 == YELLOW) ->
            GREEN

        (c1 == BLUE && c2 == VIOLET) ||
        (c1 == VIOLET && c2 == BLUE) ->
            INDIGO

        else -> throw Exception("Dirty color")

    }

println(mixOptimized(BLUE, YELLOW)) // GREEN

5. 스마트 캐스트: 타입 검사와 타입 캐스트를 조합

어떤 변수가 원하는 타입인지 is로 검사하고 나면 굳이 변수를 해당 타입으로 변환하지 않고 컴파일러가 수행하는 기능이다.

Java Style

interface Expr
class Num(val value: Int): Expr
class Sum(val left: Expr, val right: Expr): Expr

fun eval(e: Expr): Int {
    if (e is Num) {
        val n = e as Num
        return n.value
    }
    if (e is Sum) {
        val s = e as Sum
        return eval(s.left) + eval(s.right)
    }
    throw IllegalArgumentException()
}

Kotlin Style

interface Expr
class Num(val value: Int): Expr
class Sum(val left: Expr, val right: Expr): Expr

fun eval(e: Expr): Int {
    when (e) { // if를 사용해도 스마트 캐스트가 똑같이 적용된다.
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        else -> throw IllegalArgumentException()
    }
}
⚠️스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 후 그 값이 바뀔 수 없는 경우에만 작동한다. 즉 클래스의 프로퍼티에 사용한다면 해당 프로퍼티는 val이어야 하고 커스텀 접근자를 사용하지 않았어야 한다. 커스텀 접근자를 구현했다면 게터를 통해 접근했을 때 어떤 값이 반환될지 모르기 때문이다. 원하는 타입으로 변환할 땐 as 키워드를 사용한다.

6. if와 when의 분기에서 블록 사용

블록이 값을 만들어내야 하는 경우 블록의 마지막 식이 블록의 결과이다. 이는 함수에 대해서는 성립하지 않는다. 식이 본문인 함수는 블록을 본문으로 가질 수 없고 블록이 본문인 함수는 반드시 return 문이 있어야 한다.

interface Expr
class Num(val value: Int): Expr
class Sum(val left: Expr, val right: Expr): Expr

fun evalWithLogging(e: Expr): Int {
    when (e) { // if를 사용해도 스마트 캐스트가 똑같이 적용된다.
        is Num -> {
            println(e.value)
            e.value // 블록이 식이므로 마지막 식인 이 줄이 평가된 값이 반환된다.
        }
        is Sum -> {
            val left = evalWithLogging(e.left)
            val right = evalWithLogging(e.right)
            println("sum: ${left} + ${right}")
            left + right // 이 식이 평가된 값이 반환된다.
        }
        else -> throw IllegalArgumentException()
    }
}

fun compare(n1: Int, n2: Int): Int {
    // 앞 숫자가 크면 1, 작으면 -1, 같으면 0을 반환하는 함수
    // 해당 블록은 본문이므로 return이 필수이다.
    return if (n1 > n2) 1 if (n1 < n2) -1 else 0
}

fun compareExpr(n1: Int, n2: Int) = if (n1 > n2) 1 if (n1 < n2) -1 else 0