6 분 소요

green.jpg

2.3 선택 표현과 처리: enum과 when

when은 자바의 switch를대치하되 훨씬 더 강력하며, 앞으로 더 자주 사용할 프로그래밍 요소라고 생각할 수 있다.

when에 대해 설명하는 과정에서 코틀린에서 enum을 선언하는 방법과 스마트 캐스트에 대해서도 살펴본다.

2.3.1 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
}
println(Color.BLUE.rgb()) //255

자바와 마찬가지로 enum은 단순히 값만 열거하는 존재가 아니다.

enum 클래스 안에도 프러퍼티나 메소드를 정의할 수 있다.

enum 클래스 안에 메소드를 정의하는 경우 반드시 enum 상수 목록과 메소드 정의 사이에 세미콜론을 넣어야 한다.

2.3.2 when으로 enum 클래스 다루기

자바의 switch와 달리 when 절에서는 각 분기의 끝에 break를 넣지 않아도 된다.

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

한 when 분기 안에 여러 값을 사용할 수 있다.

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

enum 상수 값을 임포트해서 enum 클래스 수식자 없이 enum 사용할 수 있다.

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

2.3.3 when과 임의의 객체를 함께 사용

코틀린에서 when은 자바의 switch보다 훨씬 더 강력하다.

분기 조건에 상수만을 사용할 수 있는 자바 switch와 달리 코틀린 when의 분기 조건은 임의의 객체를 허용한다.

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

2.3.4 인자 없는 when 사용

이전의 함수는 비교대상을 Set 인스턴스를 생성한다. 보통은 이런 비효율성이 크게 문제가 되지 않는다. 하지만 이 함수가 아주 자주 호출된다면 불필요한 가비지 객체가 늘어나는 것을 방지하기 위해 함수를 고쳐 쓰는 편이 낫다.

fun mixOptimized(c1: Color, c2: Color) =
    when { //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")
    }

추가 객체를 만들지 않는다는 장점이 있지만 가독성은 더 떨어진다.

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

코틀린에서는 is를 사용해 변수 타입을 검사한다. is 검사는 자바의 instanceof와 비슷하다.

하지만 자바에서 어떤 변수의 타입을 instanceof로 확인한 다음에 그 타입에 속한 멤버에 접근하기 위해서는 명시적으로 변수 타입을 캐스팅해야 한다.

코틀린에서는 프로그래머 대신 컴파일러가 캐스팅을 해준다. 이를 스마트캐스트라고 부른다.

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

스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수 없는 경우에만 작동한다.

클래스의 프로퍼티에 대해 스마트 캐스트를 사용한다면 그 프로퍼티는 반드시 val이어야한다.

해당 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확신할 수 없기 때문이다.

2.3.6 리팩토링: if when으로 변경

코틀린에서는 if가 값을 만들어내기 때문에 자바와 달리 3항 연산자가 따로 없다.

fun eval(e: Expr): Int =
    if (e is Num) {
        e.value
    } else if (e is Sum) {
        eval(e.right) + eval(e.left)
    } else {
        throw IllegalArgumentException("Unknown expression")
    }

when 식을 앞에서 살펴본 값 동등성 검사가 아닌 다른 기능에도 쓸 수 있다.

fun eval(e: Expr): Int =
    when (e) {
        is Num ->
            e.value
        is Sum ->
            eval(e.right) + eval(e.left)
        else ->
            throw IllegalArgumentException("Unknown expression")
    }

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

if나 when 모두 분기에 블록을 사용할 수 있다. 그런 경우 블록의 마지막 문장이 블록 전체의 결과가 된다.

fun eval(e: Expr): Int =
    when (e) {
        is Num -> {
            println("num: ${e.value}")
            e.value
        }
        is Sum ->
            eval(e.right) + eval(e.left)
        else ->
            throw IllegalArgumentException("Unknown expression")
    }

2.4 대상을 이터레이션: while과 for 루프

코틀린의 while루프는 자바와 동일하다. for는 자바의 for-each 루프에 해당하는 형태만 존재한다.

2.4.1 while 루프

코틀린에는 while과 do-while 루프가 있다. 두 루프의 문법은 자바와 다르지 않다.

while(조건){
    /*...*/
}

do{
    /*...*/
}while(조건)

2.4.2 수에 대한 이터레이션: 범위와 수열

코틀린에는 자바의 for 루프에 해당하는 요소가 없다.

이런 루프의 가장 흔한 용례인 초깃 값, 증가 값, 최종 값을 사용한 루프를 대신하기 위해 코틀린에서는 범위를 사용한다.

for(i in 1..100){
    println(i)
}

반대로 100부터 거꾸로 세되 짝수만으로 게임을 진행되도록 만들어보자.

fun main(args: Array<String>) {
    for (i in 100 downTo 1 step 2) {
        println(i)
    }
}

끝 값을 포함하지 않는 반만 닫힌 범위에 대해 이터레이션하기를 원하면 until 함수를 사용하라.

예를들어

for(x in 0 until size){
    /*...*/
}
//둘다 같다.
for(x in 0..size-1){
    /*...*/
}

다음과 같이 좀 더 명확하게 개념을 표현한다.

2.4.3 맵에 대한 이터레이션

연산자를 숫자 타입의 값뿐 아니라 문자 타입의 값에도 적용할 수 있다.

for(c in 'A'..'F'){ //A부터 F까지 반복
    println(c)
    val binary = Integer.toBinaryString(c.Int()) //아스키 코드
    binaryReps[c] = binary
}

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

맵이 아닌 컬렉션에도 활용할 수 있다.

원소의 현재 인덱스를 유지하면서 컬렉션을 이터레이션할 수 있다.

인덱스를 저장하기 위한 변수를 별도로 선언하고 루프에서 매번 그 변수를 증가시킬 필요가 없다.

val list = arrayListOf("10","11","1001")

for((index, element) in list.withIndex()){
    println("$index: $element")
}
//0: 10
//1: 11
//2: 1001

2.4.4 in으로 컬렉션이나 범위의 원소 검사

in 연산자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있다.

반대로 !in을 사용하면 어떤 값이 범위에 속하지 않는지 검사할 수 있다.

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

when 식에서도 사용해도 된다.

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

2.5 코틀린의 예외 처리

코틀린의 예외처리는 자바나 다른 언어의 예외 처리와 비슷하다.

함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있다.

자바와 달리 코틀린의 throw는 식이므로 다른 식에 포함될 수 있다.

// 조건이 참이면 number의 값이 초기화되고 거짓이면 초기화되지 않고 throw를 호출한다. 
val number = try {
    Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
    return // 예외가 발생한 경우 catch 블록 다음의 코드는 실행되지 않는다. 
}

2.5.1 try, catch, finally

자바 코드와 가장 큰 차이는 throws절이 코드에 없다는 점이다.

자바에서는 함수를 작성할 떄 함수 선언 뒤에 throws IOException을 붙여야 한다. (IOExption이 체크 예외이기 때문이다.)

코틀린에서는 함수가 던지는 예외를 지정하지 않고 발생한 예외를 잡아내도 되고 잡아내지 않아도 된다.

자바 7의 자원을 사용하는 try-with-resoucr는 어떨까? 코틀린은 그런 경우를 위한 특별한 문법을 제공하지 않는다. 하지만 라이브러리 함수로 같은 기능을 구현한다. (해당 내용은 8.2.5절에서 살펴보기로 한다.)

2.5.2 try를 식으로 사용

이전의 예제를 다시 살펴보자

// 조건이 참이면 number의 값이 초기화되고 거짓이면 초기화되지 않고 throw를 호출한다. 
val number = try {
    Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
    return // 예외가 발생한 경우 catch 블록 다음의 코드는 실행되지 않는다. 
}

코틀린의 try 키워드는 if나 when과 마찬가지로 식이다. 따라서 try의 값을 변수에 대입할 수 있다.

다른 문장과 마찬가지로 try의 본문도 내부에 여러 문장이 있으면 마지막 식의 값이 전체 결과 값이다.

2.6 요약

  • 함수를 정의할 때 fun 키워드를 사용한다.
  • val, var는 각각 읽기 전용 변수와 변경 가능한 변수를 선언할 때 쓰인다.
  • 문자열 템플릿을 사용하면 문자열을 연결하지 않아도 되므로 코드가 간결해진다.
  • 변수 이름 앞에 $를 붙이거나, 식을 ${식}처럼 ${ }로 둘러싸면 변수나 식의 값을 문자열 안에 넣을 수 있다.
  • 코틀린에서는 값 객체 클래스를 아주 간결하게 표현할 수 있다.
  • 다른 언어에도 있는 if는 코틀린에서 식이며, 값을 만들어낸다.
  • 코틀린 when은 자바의 switch와 비슷하지만 더 강력하다.
  • 어떤 변수의 타입을 검사하고 나면 굳이 그 변수를 캐스팅하지 않아도 검사한 타입의 변수처럼 사용할 수 있다. 그런 경우 컴파일러가 스마트 캐스트를 활용해 자동으로 타입을 바꿔준다.
  • for, while, do-while 루프는 자바가 제공하는 같은 키워드의 기능과 비슷하다. 하지만 코틀린의 for는 자바의 for 보다 더 편리하다. 특히 맵을 이터레이션하거나 이터레이션하면서 컬렉션의 원소와 인덱스를 함꼐 사용해야 하는 경우 코틀린의 for가 더 편리하다.
  • 1..5와 같은 식은 범위를 만들어낸다. 범위와 수열은 코틀린에서 같은 문법을 사용하며, for 루프에 대해 같은 추상화를 제공한다. 어떤 값이 범위 안에 들어있거나 들어있지 않은지 검사하기 위해서 in이나 !in을 사용한다.
  • 코틀린 예외 처리는 자바와 비슷하다. 다만 코틀린에서는 함수가 던질 수 있는 예외를 선언하지 않아도 된다.

참조

드미트리 제메로프 · 스베트라나 이사코바, 『Kotlin IN ACTION』, 오현석 옮김, 에이콘(2017), P77-101.

카테고리:

업데이트: