Kotlin 기초 - 선택 표현과 처리, 이터레이션
아래 내용은 Kotlin in Action 도서를 기반으로 작성하였습니다.
선택 표현과 처리: enum과 when
when은 자바의 switch를 대체하되 훨씬 강력하며, 앞으로 더 자주 사용하게 된 프로그래밍 요소 중 하나이다.
enum
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
코틀린에서 enum은 소프트 키워드(Soft keyword, 특정 단어 앞에서 특별한 의미를 지니지만 다른 곳에서는 이름으로 쓸 수 있다)이다. 반면 class는 키워드(이름으로 사용할 수 없다)이다.
자바와 마찬가지로 코틀린의 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); //Require semicolon!!
fun rgb() = (r * 256 + g) * 256 + b
}
>>> println(Color.BLUE.rgb())
255
enum도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 선언한다. 프로퍼티가 있을 경우 각 상수를 정의할 때 그 상수에 해당하는 프로퍼티 값을 지정해야 한다. enum 클래스 안에 메소드가 있을 경우 상수 목록과 메소드 사이에 세미콜론을 넣어야 한다.
when으로 enum 클래스 다루기
앞서 다뤘듯 when은 if와 마찬가지로 식이다. ‘Richard Of York Gave Battle In Vain!’ 예시를 이용해 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"
}
위 코드는 color로 전달된 값과 같은 분기를 찾는다. 자바와 달리 분기의 끝에 break 를 넣지 않아도 된다(이거 빼먹는 실수를 하는 경우가 생각보다 많다).
한 분기 안에서 여러 값을 매치시킬 수 있다. 그럴 경우 값 사이를 콤마(,)로 분리한다.
fun getWarmth(color: Color) = when(color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}
상수 값을 임포트하면 코드를 간단하게 만들 수 있다.
import Color.*
fun getWarmth(color: Color) = when(color) {
RED, ORANGE, YELLOW -> "warm"
GREEN -> "neutral"
BLUE, INDIGO, VIOLET -> "cold"
}
임의의 객체를 사용하는 when
분기 조건에 상수(enum 상수나 숫자 리터럴 등)만을 사용할 수 있는 switch와 다르게 코틀린의 when의 분기 조건은 임의의 객체를 허용한다. 두 색을 혼합했을 때 미리 정해진 팔레트에 들어있는 색이 되는지 알려주는 함수를 작성해보자.
fun mix(c1: Color, c2: Color) =
when(setOf(c1, c2)) { //when 식의 인자로 아무 객체나 사용 가능
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color") //매치되는 분기 조건이 없으면 이 문장을 실행한다.
}
setOf()는 우선은 순서를 신경쓰지 않는 집합이라고 생각하자.
when은 매치하는 조건 값을 찾을 때까지 각 분기를 검사하며 이 과정에서 동등성(Equality)을 사용한다.
인자 없는 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")
}
when에 아무 인자도 없으면 각 분기의 조건이 Boolean 결과를 계산하는 식이어야 한다.
when 분기에서 블록 사용
when의 각 분기에는 블록을 사용할 수 있다. 이때 블록의 마지막 문장이 블록 전체의 결과가 된다.
fun mixOptimized(c1: Color, c2: Color) = when {
(c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> {
println("Orange")
ORANGE //Block Result
}
(c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) -> {
println("Green")
GREEN
}
(c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> {
println("Indigo")
INDIGO
}
else -> throw Exception("Dirty color")
}
이터레이션: while, for
이터레이션(Iteration) 조건을 만족할 때까지 명령을 반복하는 것을 의미한다.
코틀린 while 루프는 자바와 동일하다. 다만 for는 자바의 for-each루프의 형태만 존재한다.
코틀린의 for는
**for
while
코틀린은 while과 do-while이 있으며 두 문법은 자바와 동일하다.
while(조건) {
/*...*/
}
조건이 참인 동안 본문을 반복 실행한다.
do {
/*...*/
} while(조건)
처음에 무조건 본문을 한번 실행한 다음 조건이 참인 동안 본문을 반복 실행한다.
범위와 수열
앞에서 설명했듯이 코틀린은 자바의 for 루프(for(int i = 0; i < 10; i++))에 해당하는 요소가 없다. 코틀린은 이 요소의 초기값, 최종값, 증감을 대체하기 위해 범위(Range)를 사용한다.
범위는 기본적으로 두 값으로 이뤄진 구간이며 시작과 끝 값을 .. 연산자로 연결해 범위를 만들 수 있다.
val oneToTen = 1..10
코틀린의 범위는 폐구간(양끝을 포함하는 구간)이다. 따라서 위 코드는 1과 10을 포함하는(1, 2, 3,…,8, 9, 10) 범위이다.
.. 연산자 대신 downTo 함수를 사용하면 역방향 수열을 만들 수 있다.
val tenToOne = 10 downTo 1
(수열) step (값) 연산을 이용해서 수열의 증감의 절댓값을 조절할 수 있다.
1..10 step 2 // 1, 3, 5, 7, 9
10 downTo 1 step 3. // 10, 7, 4, 1
반만 닫힌 범위(시작은 포함, 끝은 포함하지 않음)를 만들고 싶다면 until 함수를 사용하면 된다.
1 until 10 // = 1..9
맵에 대한 이터레이션
val binaryRefs = TreeMap<Char, String>()
for(c in 'A'..'F') {
val binary = Integer.toBinaryString(c.code)
binaryRefs[c] = binary
}
for((letter, binary) in binaryRefs) {
println("$letter = $binary")
}
A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110
위 코드에서 알 수 있는 것은
- .. 연산자는 문자 타입의 값에도 적용할 수 있다.
- 구조 분해 문법(추후 다룰 예정)을 이용하여 이터레이션하려는 컬렉션의 원소를 분리할 수 있다.
구조 분해 구문은 맵이 아닌 컬렉션에서도 사용할 수 있다. 예를 들면 원소의 인덱스와 값을 같이 가져올 때 인덱스를 저장하기 위한 변수를 별도로 선언할 필요가 없다.
val list = arrayListOf("10", "11", "1001")
for((index, element) in list.withIndex()) {
println("$index: $element")
}
0: 10
1: 11
2: 1001
in으로 컬렉션이나 범위의 원소 검사
in을 사용해서 어떤 값이 범위에 속하는지 검사할 수 있다. 반대로 !in을 사용하면 범위에 속하지 않는지 검사할 수 있다.
c in 'a'..'z' // 'a' < c && c <= 'z'와 같다.
위 코드는 c가 알파벳 소문자인지 검사하는 코드이다.
in, !in 연산자를 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"
}
범위는 문자에만 국한되지 않는다. 비교가 가능한 클래스라면(java.lang.Comparable
Interface를 구현한 클래스라면) 범위를 만들 수 있다. String 또한 이를 구현하고 있기에(두 문자열을 알파벳 순서대로 비교함) String도 in 검사에 사용할 수 있다.
"Kotlin" in "Java".."Scala" // "Java" <= "Kotlin" && "Kotlin" <= "Scala"와 같다.
컬렉션도 in 연산을 사용할 수 있다(자바의 contains
와 비슷하게 동작).
"Kotlin" in setOf("Java", "Scala") //컬렉션에 원소가 들어있는지 검사