본문으로 바로가기
반응형

이 글은 Kotlin In Action을 참고 하였습니다.

더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다

2.3 Enum 과 When

when은 if를 대체할 수 있는 강력한 수단이며, kotlin에서 enum을 어떻게 사용하는지 알아보도록 하지요~
그리고 smart cast에 대한 얘기도 해보겠습니다.


2.3.1. enum

enum은 java와 사용법이 비슷합니다.

단 시작을 enum class xxx로 해야합니다.

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}


물론 내부에 생성자나 함수를 넣을 수 있습니다.

enum 생성과 함께 생성자를 만들어 넣어보겠습니다.

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

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

fun main(args: Array<String>) {
    println(Color.BLUE.rgb())
}

멤버 변수인 r, g, b를 생성자에 넣었습니다.

자바와 그다지 다르지 않습니다.


2.3.2 when으로 enum 다루기

when은 if나 switch를 대체할 수 있는 강력한 도구 입니다.

아래와 같이 when에 enum을 넣어 사용할 수 있습니다. (사실 이건 자바도 가능하죠)

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

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

fun main(args: Array<String>) {
    println(getMnemonic(Color.BLUE))
}


when은 break문이 필요 없습니다.

따라서 해당 라인을 만나면 수행하고 바로 when문을 빠져나옵니다.

만약 여러값을 묶어서 사용하려면 아래와 같이 , 로 구분해 주면 됩니다.

fun getWarmth(color: Color) = when(color) {
    Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
    Color.GREEN -> "neutral"
    Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}


2.3.3 When의 인자값

When은 swith와 다르게 인자값으로 아무 객체나 사용할 수 있습니다.

좀 막강하죠?

아래 코드처럼 조건에 객체가 들어가면 분기조거네 있는 객체와 같은지를 확인하여 처리합니다.

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")
        }

fun main(args: Array<String>) {
    println(mix(BLUE, YELLOW))
}

setOf는 collection중 set을 만드는 함수 입니다.

main을 보시면 BLUE와 YELLOW를 넣었습니다.

그럼 setOf(BLUE, YELLOW)가 될덴데, 실행하면 어떤색을 반환할까요?

실제 when의 분기에는 setOf(BLUE, YELLOW)로 원소가 서로 뒤바껴 있습니다.


사실 set은 원소나열의 순서는 상관없는 collection입니다.

따라서 순서가 뒤바꼈지만 GREEN이 print 됩니다. 똑똑하죠?

When 내부에는 Expression을 넣을수 있기 때문에 범용성이 훨씬 커집니다.


2.3.4 인자없는 When

When의 인자로 아무것도 넣지 않을수도 있습니다.

위 예제는 set을 계속 생성했는데, 그럴 필요없도록 만들어 보겠습니다.

(단! 코드는 좀 지저분해 집니다.)

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")
    }

when에 인자값이 없다면 조건은 boolean 이어야 합니다.


2.3.5 스마트 캐스트 smart cast

smart cast를 이용하면 타입검사와 casting을 동시에 할 수 있습니다.

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) {
        return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown expression")
}

fun main(args: Array<String>) {
    println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
}

Num과 Sum 클래스는 Expr interface를 상속 받습니다.

따라서 eval() 함수를 호출하면 인자로 들어온 Expr을 확인하여 어떤 값으로 처리할지를 확인하기 위해 "is"를 사용합니다.

"is"는 java의 instanceof와 같은 역할을 합니다.

그리고 "as"는 강제로 casting하는 역할을 합니다. 자바로 본다면 Num n = (Num) e 라고 할수 있겠네요.

이렇게 강제로 casting을 하는 이유는 casting을 해야 해당 인자(value)에 접근할 수 있기 때문입니다.


단 코틀린에서는 "is" 함수로 체크한 후에는 컴파일러가 알아서 캐스팅을 해줍니다.

따라서 "as"를 쓰지않아도 해당 블럭안에서 value에 접근할 수 있습니다.


위 코드중 "if (e is Sum) {.." 의 블럭안에서는 casting 없이 바로 right, left 변수에 접근할 수 있도록 해주는걸 스마트 캐스팅이라 합니다.

추가적으로 스카트 캐스팅을 하려면 is로 검사하는 변수값은 불변(val) 이어야 합니다. 또한 접근자를 custom하게 만들어도 스마트 캐스팅은 되지 않습니다. 


2.3.7 if를 when으로 refactoring

위 코드의 if를 제거하면서 when으로 바꿔 보겠습니다.

추가적으로 when에 multi line을 사용하려면 {...}를 사용하면 됩니다.

fun evalWithLogging(e: Expr): Int =
    when (e) {
        is Num -> {
            println("num: ${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("Unknown expression")
    }

fun main(args: Array<String>) {
    println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))))
}

여기서 블럭의 마지막라인의 값이 블럭의 결과값이 됩니다.


2.4 while과 for문

2.4.1 while loop

while과 do while은 자바와 문법이 같습니다. 패쓰~


2.4.2 iteration, range

코틀린에서는 for문을 자바처럼 쓰지 않습니다.

초기값, 조건, 증가식을 넣는 일반적인 반복문을 지원하지 않으므로 범위를 나타내는 방법을 따로 사용합니다.

val oneToTen = 1..10

이건 1,2,3......10까지 열개의 수를 나타내는 range 입니다.

".."은 폐구간을 나타내는 방법으로 마지막수를 포함합니다.

따라서 for문은 아래와 같이 사용합니다.

for (i in 100..200) {
    // i가 100 부터 200(포함)까지 반복
}

for (i in 100 downTo 1) {
    // i가 100부터 1(포함)까지 반복
}

for (i in 100 downTo 1 step 2) {
    // i가 100부터 1(포함)까지 2단계씩 뛰면서 (-2씩 증가) 반복
}

사실 우리는 끝값은 사용하지 않는 형태가 더 익숙합니다.

그때는 "until"을 사용하면 됩니다.

for (i in 1 until size) {
    //
}

// 위와 동일
for (i in 1..size-1) {
    //
}


2.4.3 Map의 iteration

.. 연산자는 숫자 뿐만 아니라 문자에도 가능합니다.

즉 'A'..'F'로 표현하면 A부터 F까지의 문자열을 의미합니다.

또한 map을 iteration할때 key와 value를 각각 받아서 반복을 돌릴 수 있습니다.

val binaryReps = TreeMap<Char, String>()

for (c in 'A'..'F') {
    val binary = Integer.toBinaryString(c.toInt())
    binaryReps[c] = binary
}

for ((letter, binary) in binaryReps) {
    println("$letter = $binary")
}

자바에서 map을 iteration할때보다 훨씬 간단하죠?

만약 List에서도 index와 함께 iteration하고 싶다면 아래와 같이 사용하면 됩니다.

val list = listOf("1","2","3")
for ((index, value) in list.withIndex()){
    println("$index : $value")
}


추가적으로 map이나 list에서 값을 넣거나 뺄때 add, put, get등의 api가 아니라 위 예제처럼 배열에 값을 넣거나 빼듯이 표현할 수 있습니다.


2.4.4 in을 이용한 범위검사

in을 이용해서 범위를 검사할 수도 있습니다.

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'

fun main(args: Array<String>) {
    println(isLetter('q'))
    println(isNotDigit('x'))
}

위 코드중 c in 'a'..'z'는 사실 표준 라이브러리에서 'a' <= 'c' && 'c' <= 'z'로 변환됩니다.


추가적으로 when 연산자에서도 in 이나 !in을 써서 분기를 만들수도 있습니다.

fun recognize(c: Char) = when (c) {
    in '0'..'9' -> "It's a digit!"
    in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
    else -> "I don't know…​"
}

fun main(args: Array<String>) {
    println(recognize('8'))
}

in으로 비교하는 범위는 비교 가능한 클래스라면 어떤것이든 가능합니다.(comparable을 구현한 클래스들..)

println("Kotlin" in "Java".."Scala")
println("Kotlin" in setOf("Java".."Scala"))


2.5 예외처리 

try-catch는 기본적으로 자바와 같습니다.

따라서 다른점만 나열해 보면

  1. throw를 던질때 new를 사용하지 않는다.
  2. 반드시 구현해야 하는 checked exception을 강요하지 않는다. (IO관련 함수 사용시 IOException등을 throws 안해도 됨.)
  3. try-with-resource는 지원하지 않는다. (대신 "use" 키워드를 제공하지만 나중에 언급하겠습니다.)
  4. try catch도 expression이므로 블럭의 마지막이 결과값이 된다.

아래 예제는 IOException을 throws 하지 않았지만 에러가 발생하지 않습니다.
자바는 이런 exception을 강제적으로 구현하도록 하였으나, 실제 개발자들이 빈 껍데기로 사용하는 경우가 더 많다는점을 반영하여 코틀린에서는 강요하지 않습니다.

fun readNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLine()
        return Integer.parseInt(line)
    }
    catch (e: NumberFormatException) {
        return null
    }
    finally {
        reader.close()
    }
}

위 코드는 사실 아래와 같이 쓸수 있습니다.

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        return
    }

    println(number)
}



반응형