선택 표현과 처리 : enum과 when

Suyong Lee·2021년 9월 3일
0

Kotlin

목록 보기
6/16
post-thumbnail

코틀린의 when은 자바의 switch에 해당한다.

근데 훨씬 더 강력하다고 한다.

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

enum 클래스 정의

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

자바에서는 enum을 사용하지만 코틀린에서는 enum class를 사용한다.

코틀린에서 그냥 enum은 소프트 키워드라 부르는 존재다.

enum 클래스 안에도 프로퍼티나 메서드를 정의할 수 있다. 다음은 프로퍼티와 메서드를 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 // enum 클래스 안에서 메서드를 정의한다.
}

println(Color.BLUE.rgb())
255

enum에서도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 선언한다. 각 enum 상수를 정의할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야만 한다. 이 예제에서는 코틀린에서 유일하게 세미콜론(;)이 필수인 부분을 볼 수 있다.

when 으로 enum 클래스 다루기

if와 마찬가지로 when도 값을 만들어내는 식이다. 따라서 식이 본문인 함수에 when을 바로 사용할 수 있다.

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

println(getMnemonic(Color.BLUE))
-> Battle

앞의 코드는 color로 전달된 값과 같은 분기를 찾는다. 자바와 달리 각 분기의 끝에 break를 넣지 않아도 된다. 성공적으로 매치되는 분기(분기란 Color.RED -> "Richard" 이 부분)를 찾으면 switch는 그 분기를 실행한다.

한 분기 안에서 여러 값 사용하기

한 분기 안에서 여러 값을 매치 패턴으로 사용할 수도 있다. 그럴 경우 값 사이를 콤마(,)로 분리한다.

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

지금까지 살펴본 두 예제에서는 Color.YELLOW 처럼 Color라는 enum 클래스 이름을 enum 상수 이름 앞에 붙인 전체 이름을 사용했다. 상수 값을 임포트하면 이 코드를 더 간단하게 만들 수 있다.

import ch02.colors.Color
import ch02.colors.Color.* // Color 클래스의 모든 요소를 임포트한다.

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

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

자바 swtich와 달리 코틀린 when의 분기 조건은 임의의 객체를 허용한다. 두 색을 혼합했을 때 미리 정해진 팔레트에 들어있는 색이 될 수 있는지 알려주는 함수를 작성하자.

fun mix(c1: Color, c2: Color) =
	when(setOf(c1, c2)) {
    	setOf(RED, YELLOW) -> ORANGE // when 식의 인자로 아무 객체나 사용할 수 있다. when은 이렇게 인자로 받은 객체가 각 분기 조건에 있는 객체와 같은지 테스트한다.
        setOf(YELLOW, BLUE) -> GREEN
        setOf(BLUE, VIOLET) -> INDIGO
        else -> throw Exception("Dirty Color") // 매치되는 분기 조건이 없으면 이 문장을 실행한다.
    }

println(mix(BLUE, YELLOW))
-> GREEN

코틀린 표준 라이브러리에는 인자로 전달받은 여러 객체를 그 객체들을 포함하는 집합인 Set 객체로 만드는 setOf라는 함수가 있다.

집합(Set)은 원소가 모여있는 컬렉션으로, 각 원소의 순서는 중요하지 않다.

따라서 setOf(c1,c2)와 setOf(RED,YELLOW) 가 같다는 ㅁ라은 c1이 RED이고 c2가 YELLOW거나, c1이 YELLOW이고 c2가 RED라는 말이다.

모든 분기 식에서 만족하는 조건을 찾을 수 없다면 else 분기의 문장을 계산한다.

임의의 불리언(boolean)을 조건식으로 사용하는 인자 없는 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

when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이어야 한다. mixOptimized 함수는 앞에서 살펴본 mix 함수와 같은 일을 한다.

인자가 없는 when은 가비지 객체를 만들지 않는다는 장점이 있지만 불리언 계산식이 길기 때문에 가독성이 떨어진다는 단점이 있다.

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

(1+2)+4 와 같은 간단한 산술식을 계산하는 함수를 만들어보자.

우선 식을 인코딩하는 방법을 생각해야 한다.

식을 트리 구조로 저장하자.

노드는 합계(Sum)나 수(Num) 중 하나다.

Num은 항상 말단노드지만, Sum은 자식이 둘 있는 중간노드다.

Sum 노드의 두 자식은 덧셈의 두 인자다.

무슨 소리지?..................일단 책에 있는 내용 베꼈음.

interface Expr
class Num(val value: Int) : Expr // value라는 프로퍼티만 존재하는 단순한 클래스로 Expr 인터페이스를 구현한다.
class Sum(val left: Expr, val right: Expr) : Expr // Expr 타입의 객체라면 어떤 것이나 Sum 연산의 인자가 될 수 있다. 따라서 Num이나 다른 Sum이 인자로 올 수 있다.

Sum은 Expr의 왼쪽과 오른쪽 인자에 대한 참조를 left와 right 프로퍼티로 저장한다.

이 예제에서 left와 right는 각각 Num이나 Sum일 수 있다. (1+2)+4 라는 식을 저장하면 Sum(Sum(Num(1),Num(2)),Num(4))라는 구조의 객체가 생긴다.

자바 스타일로 작성한 함수를 먼저 살펴본 다음 코틀린 스타일로 만든 함수를 살펴보자.

자바였다면 조건을 검사하기 위해 if문을 사용했을 것이다.

따라서 코틀린에서 if를 써서 자바 스타일로 함수를 작성해보자.

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

println(eval(Sum(Sum(Num(1),Num(2)), Num(4))))
-> 7

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

어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다. 하지만 실제로는 컴파일러가 캐스팅을 수행해준다. 이를 스마트캐스트 라고 부른다.

if (e is Sum) {
	return eval(e.right) + eval(e.left)
}

리팩토링 : 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")
    }

println(eval(Sum(Num(1),Num(2))))
-> 3

if 중첩 대신 when 사용한 코드

fun eval(e: Expr) :Int = 
	when (e) {
    	is Num -> // 인자 타입을 검사하는 when 분기들
        	e.value
        is Sum ->
        	eval(e.right) + eval(e.left) // 이 부분에서 스마트 캐스트가 쓰였다.
        else ->
        	throw IllegalArgumentException("Unknown expression")
    }

if와 when 분기에서 블록 사용

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

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

evalWithLogging 함수가 출력하는 로그를 보면 연산이 이뤄진 순서를 알 수 있다.

println(evalWithLogging(Sum(Sum(Num(1),Num(2)),Num(4))))

-> num : 1
-> num : 2
-> sum : 1 + 2
-> num : 4
-> sum : 3 + 4
-> 7

식이 본문인 함수는 블록을 본문으로 가질 수 없고 블록이 본문인 함수는 내부에 return문이 반드시 있어야 한다.

이 포스팅은 사실 반밖에 이해가 안 감. 일단 쭉 보고 나중에 와서 다시 봐야할듯

profile
이수용

0개의 댓글

관련 채용 정보