Studying/Kotlin

[kotlin in action] 선택 표현과 처리: enum과 when (스마트캐스트)

강옌니 2024. 2. 29. 04:43

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

  • when은 자바의 switch을 대치하되 훨씬 더 강력하고, 자주 사용할 프로그래밍 요소

자바의 switch

 

[ 기존의 switch ]

switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

 

기존 자바 코드에서는 fall through를 사용해서 case MONDAY, FRIDAY, SUNDAY인 경우에 6을 출력

  • fall through: 하나의 case 절에서 break 문을 사용하지 않고 다음 case 절로 코드 실행 흐름이 이어지는 것을 의미

 

[ 개선된 switch ]

switch (day) {
    case MONDAY, FRIDAY, SUNDAY	-> System.out.println(6);
    case TUESDAY				-> System.out.println(7);
    case THURSDAY, SATURDAY		-> System.out.println(8);
    case WEDNESDAY				-> System.out.println(9);
}

 

  • fall through를 사용하지 않고 다중 case 라벨(,)을 사용하여 case MONDAY, case FRIDAY, case SUNDAY를 표현한 것이 훨씬 가독성이 좋아졌다.
  • 그리고 break문을 사용하지 않아도 화살표 case 라벨 → 을 사용한 것과 동일하다.
  • 이때, 주의해야할 점은 case 라벨 : 을 사용할 때는 실행문이 여러개여도 중괄호를 사용하지 않아도 되지만,  화살표 case 라벨 → 을 사용할 때는 중괄호를 사용해야한다.

 

switch문에 대해서는 이정도만 간단히 알아두면 좋을 것 같아서 정리했다. 자세한 내용은 밑에 링크 보면서 참고해도 좋을 것 같다.

 

[Java 14] 개선된 switch 문(Enhanced Switch Expressions)

1. 개선된 switch 문이란? 2020년 3월에 출시된 Java 14부터 개선된 switch 문을 지원합니다. 기존 switch문은 깔끔하지 못하고 가독성도 떨어지며, break문의 누락으로 인한 오류 가능성도 크기 때문에 화

congcoding.tistory.com

 

 

2.3.1 enum 클래스 정의

 

// 간단한 enum 클래스 정의

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

 

 enum은 자바 선언보다 코틀린 선언에 더 많은 키워드를 써야하는 흔치 않은 예다. 코틀린은 enum class를 사용하지만, 자바에서는 enum을 사용한다.

 enum은 열거형, 구성요소를 정의하는 값, 타입이 필요할 때 사용한다.

 코틀린에서는 enum은 소프트 키워드라 부르는 존재다.  enum은 class 앞에 있을 때는 특별한 의미를 지니지만 다른 곳에 이름으로 사용이 가능하다는 말이다.

 

// 프로퍼티와 메서드가 있는 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())
225

 

  • enum에서도 일반적인 클래스와 같이 생성자와 프로퍼티를 선언한다.
  • 상수를 정의할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야 한다.
  • enum 클래스에서는 메서드를 정의하는 경우에는 반드시 상수 목록과 메서드 정의 사이에 세미콜론을 넣어야 한다.

 

2.3.2 when으로 enum 클래스 다루기

 

// when을 사용해 올바른 enum 값 찾기

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로 전달된 값과 같은 분기를 찾는 코드이다.

 

// 한 when 분기 안에 여러 값 사용하기

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.Orange))
warm

 

  • 한 분기 안에서 여러 값을 매치 패턴으로 사용 할 수 있다. 그럴 경우 위의 코드처럼 콤마(,)로 분리한다.

 

import ch02.colors.Color // 다른 패키지에서 저의한 Color 클래스를 임포트한다.
import ch02.colors.Color.* // 짧은 이름으로 사용하기 위해 enum 상수를 모두 임포트한다.

fun getWarmth(color: Color) = when(color) {
	RED, ORANGE, YELLOW -> "warm" // 임포트한 enum 상수를 이름만으로 사용한다.
	GREEN -> "neutral"
	BLUE, INDIGO, VIOLET -> "cold"
}

 

  • enum 클래스의 상수 값을 임포트하면 코드를 더욱 간단하게 만들 수 있다.

 

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

 

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

 

// when의 분기 조건에 여러 다른 객체 사용하기

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

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

 

 

집합 비교를 사용한 코드이다.

  • 코틀린 표준 라이브러리에 있는 인자로 전달받은 여러 객체를 그 객체들을 포함하는 집합인 Set 객체로 만드는 setOf라는 함수를 사용했다.
  • 집합(set)은 원소가 모여 있는 컬렉션으로, 각 원소의 순서는 중요하지 않다.
  • → setOf(RED, YELLOW) setOf(YELLOW, RED) 똑같다
  • when 식은 인자 값이 매치하는 조건 값을 찾을 때까지 각 분기를 검사한다.
    • 여기서는 setOf(c1, c2)와 분기 조건에 있는 객체 사이를 매치할 때 동등성(equility)을 사용한다.
    → 처음 분기 조건과 같지 않으면 다음 분기 조건를 차례로 비교하는 식으로 작동, 모든 분기 식에서 만족하는 조건을 찾을 수 없다면 else 분기의 문장을 계산한다.

 

  • when의 분기 조건 부분에 식을 넣을 수 있기 때문에 많은 경우 코드를 더 간결하고 아름답게 작성할 수 있다고 한다.

 

2.3.4 인자 없는 when 사용

 

위의 코드는 약간 비효율적이다. 이 함수가 호출될 때마다 함수 인자로 주어진 두 색이 when의 분기 조건에 있는 다른 두 색과 같은지 비교하기 위해 여러 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

 

 

인자 없는 when식을 사용하면 불필요한 객체 생성을 막을 수 있지만, 코드는 약간 읽기 어려워 질 수 있다.

  • when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이여야 한다.

 

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

 

산술식을 계산하는 함수를 만들어본다. 식을 인코딩하는 방법은 트리 구조로 저장한다.

  • 노드는 합계나 수 중 하나이며, 수는 항상 말단 노드지만, sum은 자식이 둘 있는 중간 노드이다.
  • sum 노드의 두 자식은 덧셈의 두 인자다.

 

// 식을 표현하는 클래스 계층

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

 

 식을 위한 Expr 인터페이스가 있다. 아무 메서드도 선언하지 않고, 단지 여러 타입의 식 객체를 아우르는 공통 타입 역할만 수행한다. 클래스 Num과 Sum이 Expr 인터페이스를 구현한다.

 Sum은 Expr의 왼쪽과 오른쪽 인자에 대한 참조를 left와 right 프로퍼티로 저장한다. 이 예제에서는 left와 right는 각각 Num이나 Sum일 수 있다. (트리표현)

 

// 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("Unkown expression")
}

>>> println(eval(Sum(Sum(Num(1), Num(2), Num(4))))
7
  • 코틀린에서는 is를 사용해 변수타입을 검사한다.
    • 코틀린에서는 프로그래머대신 컴파일러가 캐스팅을 해준다. → 스마트 캐스트
    • 어떤 변수가 원하는 타입인지 일단 is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다.
    • 하지만 스마트 캐스트는 그 값이 변할 수 없는 경우에만 가능한다. 프로퍼티가 반드시 val 이여야 하고, 커스텀 접근자를 사용한것이어도 안된다. 이런 접근들은 항상 같은 값을 내놓는다고 확신할 수 없기 때문이다.
    • 원하는 타입으로 명시적 타입 캐스팅하려면 as 키워드를 사용한다.
  • 스마트 캐스트에 대한 공식 홈페이지 내용을 다시 정리한 글을 첨부합니다.
 

[kotlin] 스마트 캐스트

is and !is operators if (obj is String) { print(obj.length) } if (obj !is String) { // Same as !(obj is String) print("Not a String") } else { print(obj.length) } is를 통해서 지정된 객체 유형에 맞는지 검사를 수행한다. Smart casts fun

yenitory.tistory.com

 

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

 

// 값을 만들어내는 if 식

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가 값을 만들어 내기 때문에 이런 특성을 사용하면 eval 함수에서 return문을 없앨 수 있다. 또한, if 분기에 식이 하나밖에 없다면 중괄호도 생략해서 본문으로 사용하여 더 간단하게 만들 수 있다.

 

// if 중첩 대신 when 사용하기

fun eval(e: Expr) : Int =
	when(e) {
		is Num ->
			e.value
		is Sum ->
			eval(e.right) + eval(e.left)
	} c
}

 

 이 예제는 앞에서 살펴본 동등성 검사가 아닌 받은 값의 타입을 검사하는 when 분기를 보여준다. 타입을 검사하고 나면 스마트 캐스트가 이뤄진다.

 

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

 

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

 

// 분기에 복잡한 동작이 들어가 있는 when 사용하기

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