Kotlin

[kotlin] 이터레이션 기초: while, for 루프

sh1mj1 2023. 12. 27. 23:48

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

 

이미지 출처    https://commons.wikimedia.org/wiki/File:Kotlin_Icon.svg

 

while 루프

코틀린에는 자바와 같은 문법의 `while` 과 `do-while` 루프가 있다.

while (조건) { // 조건이 참인 동안 본문을 반복 실행
    /* ... */
}

do {
    /* ... */
} while (조건) // 맨 처음에 무조건 본문을 한 번 실행, 그 후 조건이 참인 동안 본문을 반복 실행

수에 대한 iteration: range(범위), progression(수열)

자바에서는 `for` 루프를 아래처럼 사용한다.

`for (어떤 변수 i 를 초기화하는 식; 각 루프 후 조건 검사 ;변수 i를 루프를 한번 실행할 때마다 갱신)`

 

코틀린에서는 `for` 루프를 위처럼을 사용할 수 없다.

하지만 이런 루프를 가장 흔히 사용하는 초기값, 증가값, 최종값을 사용한 루프를 대신하기 위해 코틀린에서는 range(범위)를 사용한다.

코틀린의 `for` 문은 C# 처럼 `for (<아이템> in <원소들>)` 형태를 취한다.

 

범위는 아래처럼 사용한다.

`val oneToTen = 1 .. 10`

코틀린에서 범위는 양 쪽 모두 닫힌 구간이다. 즉, `1 .. 10` 은 1과 10 이 포함된다.

어떤 범위에 속한 값을 일정 순서로 iteration 하는 경우를 progression(수열) 이라고 한다.

 

`when` 을 이용해 Fizz-Buzz 게임을 구현하고 범위를 사용한 반복문으로 그 게임을 사용해보자.

3 으로 나누어 떨어지는 수는 Fizz, 5 로 나누어 떨어지는 수는 Buzz, 둘 모두 나누어 떨어지는 수는 FizzBuzz 가 되는 것이다.

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz  "
    i % 3 == 0 -> "Fizz  "
    i % 5 == 0 -> "Buzz  "
    else -> "$i  "
}

증가 값 `step` 을 갖는 수열에 대해 iteration 하도록 만들 수도 있다.

`downTo` 라는 키워드를 통해 역방향 수열을 만들 수도 있다. `100 downTo 1` 은 역방향 수열을 만들며 이 때는 기본 `step` 은 -1 이다.

만약 `100 downTo 1 step 2` 라고 하면 증가값의 절대값은 2가 되며, 실제 증가값은 -2 가 된다.

 

`..` 는 항상 닫힌 범위이다. 만약 오른쪽값을 포함하지 않도록 하려면 `until` 을 사용하면 된다.

 `for (x in 0 until size)` 처럼 사용하면 된다. 이는 `for (x in 0 .. size-1)` 과 같은 의미이다.

map 에 대한 iteration

코틀린에서도 컬렉션에 대한 이터레이션을 위해 `for .. in ... ` 루프를 사용하며 자바와 똑같이 동작한다.

 

`map` 에 대한 `iteration` 을 보자.

fun mapIter() {
    val binaryReps = TreeMap<Char, String>() // 키에 대해 정렬하기 위해 TreeMap 사용

    for (c in 'A'..'F') { // A 부터 F 까지 문자의 범위를 사용해 iteration
        val binary = Integer.toBinaryString(c.code) // ASCII 코드를 2진 표현으로 바꿈
        binaryReps[c] = binary 
    }

    for ((letter, binary) in binaryReps) { // map 에 대해 iteration
        println("$letter: $binary") 
    }
}

위 메서드는 문자에 대해 이진 코드를 출력하는 메서드이다.

결과:
A: 1000001
B: 1000010
C: 1000011
D: 1000100
E: 1000101
F: 1000110

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

또한 `binaryReps[c] = binary` 라는 코틀린 코드는 `binaryReps.put(c, binary)` 라는 자바 코드와 같다.

 

위에서 `(letter, binary) in binaryReps)` 라는 형태로 구조 분해 구문을 사용했던 것처럼

`map` 이 아닌 다른 컬렉션에도 활용할 수 있다.

fun listIter() {
    val list = arrayListOf("10", "11", "1001")
    for ((index, element) in list.withIndex()) { // 인덱스와 함께 컬렉션을 iteration
        println("$index: $element")
    }
}

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

결과
0: 10
1: 11
2: 1001

in 으로 컬렉션, 범위의 원소 검사하기

`in` 연산자를 사용해서 어떤 값이 범위에 속하는지 검사하고, `!in` 을 사용하여 어떤 값이 범위에 속하지 않는지 검사할 수 있다.

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

println(isLetter('q'))
// true
println(isNotDigit('x'))
// true

`c in 'a' .. 'z'` 코드는 실제로는 비교 로직 ` 'a' <= c && c <= 'z'` 로 변환된다.

실제 비교 로직은 표준 라이브러리의 범위 클래스 구현 안에 깔끔히 감춰져 있다.

 

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

println(recognize('8'))
// It's a digit!

범위는 문자에만 국한되지 않으며 비교가 가능한 클래스라면(`java.lang.Comparable` 인터페이스를 구현한 클래스라면) 그 클래스의 인스턴스 객체를 사용해 범위를 만들 수 있다.

`Comparable` 을 사용하는 범위의 경우, 그 범위 내의 모든 객체를 항상 이터레이션하지는 못한다.

('Java' 와 'Kotlin' 사이의 모든 문자열을 이터레이션할 수 없음)

하지만 `in` 연산자를 사용하여 값이 범위 안에 속하는지는 항상 결정할 수 있다.

println("Kotlin" in "Java" .. "Scala")
// true

위 `in` 식은 `"Java" <= "Kotlin" && "Kotlin" <= "Scala"`와 같다.

`String` 의 `Comparable` 구현에서는 두 문자열을 알파벳 순서로 비교하기 때문에 `in` 검사에서도 마찬가지로 문자열을 알파벳 순서로 비교한다.

컬렉션에도 마찬가지로 `in` 연산을 사용할 수 있다.