코틀린 베이직 KB 10 범위와 반복 — for 문에 세미콜론이 없다
코틀린 베이직 · 10/26

KB 10 범위와 반복 — for 문에 세미콜론이 없다

gabury1고친 사람 github-actions[bot]

0. 첫 예제: for (int i = 0; i < n; i++) 가 없다

자바 개발자 손에 가장 익은 반복문은 아마 이것입니다.

Java
public class Count {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            System.out.println(i);
        }
    }
}

0, 1, 2 를 한 줄씩 찍습니다. 이 for 줄을 코틀린 파일 Count.kt 의 main 안에 옮겨 코틀린 컴파일러 kotlinc 에 넘겼습니다. -d out 은 결과물을 out 폴더에 쓰라는 옵션입니다.

터미널
$ kotlinc Count.kt -d out
Count.kt:2:5: error: only expressions are allowed here.
    for (int i = 0; i < 3; i++) {
    ^^^^^^^^^^^
...
Count.kt:2:14: error: syntax error: Expecting 'in'.
    for (int i = 0; i < 3; i++) {
             ^
...

에러가 열두 개 나왔습니다. ... 로 줄인 두 곳에 열 개가 더 있습니다.

여기서 볼 것은 하나입니다. Count.kt:2:14 는 2번째 줄 14번째 칸이고, ^ 표시도 i 를 가리킵니다. 컴파일러는 int 를 반복 변수의 이름으로 읽었습니다. 그러고는 그 다음 칸(i 가 있는 곳)에 in 이 와야 한다고 했습니다. 코틀린의 for 에는 세미콜론 세 칸짜리 모양이 없습니다. for (변수 in 무엇) 한 가지 모양뿐입니다.

같은 일을 코틀린답게 쓰면 이렇습니다. Count2.kt 로 저장해 돌렸습니다.

Kotlin
fun main() {
    for (i in 0 until 3) {
        println(i)
    }
}
터미널
$ kotlinc Count2.kt -d out
$ kotlin -cp out Count2Kt
0
1
2
  • fun main() — 프로그램 시작점인 함수입니다. fun 은 함수를 선언하는 키워드입니다
  • for (i in 0 until 3) — 0 until 3 이 만드는 0, 1, 2 를 차례로 i 에 넣으며 돕니다. i 앞에 int 도 val 도 쓰지 않습니다
  • println(i) — 한 줄 출력입니다. 줄 끝에 세미콜론이 없습니다
  • kotlin — 코틀린 설치에 딸린 실행 명령입니다. java 와 같은 일을 하되 코틀린 표준 라이브러리(kotlin. 으로 시작하는 클래스가 든 jar)를 classpath 에 스스로 넣습니다
  • Count2Kt — 클래스 밖에 쓴 함수를 담으려고 컴파일러가 파일 이름 + Kt 로 지은 클래스입니다

자바와 출력이 같습니다. 그러면 궁금한 것이 셋 생깁니다. 0 until 3 은 무엇인가. in 오른쪽에는 무엇이 올 수 있나. 이 반복이 자바의 인덱스 반복과 같은 코드로 도는가. 이 편은 그 셋을 차례로 풉니다. 출력은 전부 Kotlin 2.4.10, JDK(자바 개발 도구 묶음) 21 에서 직접 돌린 결과입니다.

1. .., ..<, until, downTo, step

0 until 3 같은 것을 코틀린에서는 범위(range)라고 부릅니다. 이 절은 범위를 만드는 다섯 가지 표기를 한 파일에 모아 돌리고, 자바의 for 와 맞대 봅니다. 코드에 쓰는 joinToString(" ") 은 범위에 든 수를 공백 하나씩 사이에 두고 이어 붙인 문자열을 돌려주는 함수입니다. 범위 안에 무엇이 들었는지 한 줄로 보려고 썼습니다.

Kotlin
fun main() {
    // 1 2 3 4 5
    println((1..5).joinToString(" "))
    // 1 2 3 4
    println((1..<5).joinToString(" "))
    // 1 2 3 4
    println((1 until 5).joinToString(" "))
    // 5 4 3 2 1
    println((5 downTo 1).joinToString(" "))
    // 1 4 7 10
    println((1..10 step 3).joinToString(" "))
    // 10 6 2
    println((10 downTo 1 step 4).joinToString(" "))
    // (빈 줄)
    println((5..1).joinToString(" "))
}

코드 줄이 길어 찍힌 값을 각 줄 위에 적었습니다.

터미널
$ kotlinc Ranges.kt -d out
$ kotlin -cp out RangesKt
  • 1..5 — 1 부터 5 까지, 양 끝을 모두 넣는 범위입니다
  • 1..<5 와 1 until 5 — 끝값 5 를 빼는 범위입니다
  • 5 downTo 1 — 5 에서 1 로 줄어드는 범위입니다
  • step 3 — 셋씩 건너뜁니다. 앞의 범위 뒤에 붙입니다

마지막 줄이 비었습니다. 5..1 은 거꾸로 세 주지 않고 빈 범위가 됐습니다. 줄어드는 범위는 반드시 downTo 로 씁니다.

자바 for 로 옮기면

다섯 표기를 자바 for 문의 괄호 안으로 옮기면 이렇습니다.

코틀린 자바 for 로 옮기면 끝값
1..5 for (int i = 1; i <= 5; i++) 넣는다
1..<5 · 1 until 5 for (int i = 1; i < 5; i++) 뺀다
5 downTo 1 for (int i = 5; i >= 1; i--) 넣는다
1..10 step 3 for (int i = 1; i <= 10; i += 3) 넣는다

until·downTo·step 은 키워드가 아니라 표준 라이브러리 함수입니다. 1.until(5) 를 점과 괄호 없이 1 until 5 로 부르게 해 주는 함수를 중위 함수(infix function)라고 부릅니다.

.. 와 ..< 은 연산자입니다. 공식 문서는 .. 이 rangeTo() 함수를, ..< 이 rangeUntil() 함수를 부른다고 소개합니다(코틀린 문서 「Ranges and progressions」).

자바 쪽은 표의 넷째 줄과 i -= 4 로 줄어드는 반복을 Ranges.java 로 돌렸습니다. 1 4 7 10 과 10 6 2 를 찍어 코틀린과 같았습니다.

반대도 마찬가지입니다. 0절에서 코틀린이 세미콜론 세 칸짜리 for 를 거절했듯, 자바는 범위 표기를 거절합니다. for (int i : 1..5) 라고 쓴 파일을 Bad.java 로 저장해 컴파일했습니다.

터미널
$ javac -d jout Bad.java
Bad.java:3: error: ')' expected
        for (int i : 1..5) {
                       ^
Bad.java:3: error: ';' expected
        for (int i : 1..5) {
                         ^
2 errors

범위는 값이다

1..9 step 3 을 반복에 쓰지 않고 변수에 담아 찍어 봤습니다.

Kotlin
fun main() {
    val r = 1..9 step 3
    println(r)          // 1..7 step 3
    println(r.first)    // 1
    println(r.last)     // 7
    println(5 downTo 1) // 5 downTo 1 step 1
    println(3 in 1..5)  // true
    println(1..10 step 0) // 예외 — 아래
}
터미널
$ kotlinc Show.kt -d out
$ kotlin -cp out ShowKt
Exception in thread "main" java.lang.IllegalArgumentException: Step must be positive, was: 0.
...
  • val r = ... — 다시 대입할 수 없는 변수입니다. 타입을 안 적으면 값에서 알아냅니다
  • r.first, r.last — 범위의 첫 값과 마지막 값입니다. 자바의 getFirst() 게터를 필드처럼 부르는 코틀린의 프로퍼티 문법입니다
  • 3 in 1..5 — 3 이 범위 안에 드는지 묻는 식입니다. for 의 in 과 글자는 같지만 여기서는 참·거짓을 돌려줍니다

9 까지라고 썼는데 1..7 step 3 이 찍혔습니다. 1 에서 셋씩 가면 1, 4, 7 다음이 10 이라 9 를 넘습니다. 그래서 만들 때 마지막 값을 7 로 미리 맞춰 둡니다. 공식 문서도 for (i in 1..9 step 3) 의 마지막 원소가 7 이라는 예를 들고, 「the last element is not always the same as the specified end value」라고 짚습니다(코틀린 문서 「Ranges and progressions」). 이 7 은 3절에서 컴파일 결과를 열 때 다시 나옵니다.

step 0 은 컴파일은 되고 실행할 때 IllegalArgumentException 으로 멈췄습니다. step 함수에는 양수만 넘깁니다.

그러면 줄어드는 방향은 무엇이 정할까요. downTo 가 속의 간격을 -1 로 두어 정합니다. 찍을 때만 5 downTo 1 step 1 처럼 양수로 보여 줍니다. 따로 (5 downTo 1).step 을 찍었더니 -1 이 나왔습니다.

찍힌 것을 모아 보면 범위가 무엇을 들고 있는지 드러납니다. r.first 와 r.last 로 첫 값과 마지막 값을 하나씩 읽었고, (5 downTo 1).step 으로 간격도 읽었습니다. 범위는 첫 값·마지막 값·간격 세 정수를 든 값입니다. 방향은 간격의 부호에 담기고, 마지막 값은 간격에 맞춰 미리 계산됩니다. 이 값이 클래스 파일에 객체로 남는 때와 남지 않는 때는 3절에서 가릅니다.

2. for in: 범위, 컬렉션, indices, withIndex

코틀린의 for 는 in 오른쪽에 범위만 받는 게 아닙니다. 자바의 향상된 for(for (String s : list))처럼 컬렉션도 받습니다. 이 절은 이름 세 개가 든 리스트를 세 가지로 돌고, 같은 일을 자바로 합니다.

Kotlin
fun main() {
    val names = listOf("kim", "lee", "park")
    for (name in names) {
        println(name)
    }
    println(names.indices)
    for (i in names.indices) {
        println("$i ${names[i]}")
    }
    for ((i, name) in names.withIndex()) {
        println("$i $name")
    }
}
터미널
$ kotlinc Each.kt -d out
$ kotlin -cp out EachKt
kim
lee
park
0..2
0 kim
1 lee
2 park
0 kim
1 lee
2 park
  • listOf(...) — 읽기 전용 리스트를 만듭니다. JVM(java 명령이 띄우는 자바 가상 머신)에서는 java.util.List 로 보입니다
  • names.indices — 리스트에서 쓸 수 있는 인덱스의 범위입니다
  • "$i ${names[i]}" — $i 와 ${ } 에 값이 끼워진 문자열입니다. names[i] 는 자바의 names.get(i) 입니다
  • names.withIndex() — 인덱스와 원소를 짝지어 돌려줍니다

println(names.indices) 가 0..2 를 찍었습니다. indices 는 1절에서 본 범위 그 자체입니다. 그래서 for (i in names.indices) 는 범위를 도는 for 와 같은 모양입니다. 마지막 for 는 withIndex 가 준 짝을 (i, name) 처럼 괄호로 받아 변수 둘로 풉니다. 이 문법을 구조 분해 선언(destructuring declaration)이라고 부릅니다.

자바로 같은 일을

자바는 앞의 두 반복을 이렇게 씁니다.

Java
List<String> names = List.of("kim", "lee", "park");
for (String name : names) {
    System.out.println(name);
}
for (int i = 0; i < names.size(); i++) {
    System.out.println(i + " " + names.get(i));
}

Each.java 의 main 에서 이 부분만 옮겼습니다. 돌리면 kim·lee·park 과 0 kim·1 lee·2 park 를 코틀린과 같게 찍었습니다. withIndex 에 해당하는 것은 자바에 없습니다. 향상된 for 를 쓰면서 인덱스도 필요하면 바깥에 int i = 0 을 두고 직접 늘려야 합니다.

자바의 어떤 for 가 되나

이 반복들을 함수로 감싸 Loops.kt 에 모아 컴파일하고, 나온 클래스 파일을 디컴파일러 CFR 0.152 로 자바 코드로 되돌려 맞대 봤습니다. 디컴파일(decompile)은 클래스 파일을 거꾸로 읽어 소스 코드 모양으로 다시 적는 일입니다. 표의 IntArray 는 자바의 int[] 에 해당하는 코틀린 타입입니다.

코틀린 CFR 결과
for (name in names) for (String name : names)
IntArray 를 도는 for (x in xs) for (int x : xs)
for (i in names.indices) size() 를 한 번 읽어 두고 도는 for (int i = 0; i < n; ++i)

코틀린의 for 는 자바의 향상된 for 한 가지만 남긴 모양입니다. 인덱스가 필요하면 indices 라는 범위를 돌거나 withIndex 로 짝을 받습니다.

3. 범위를 도는 for 는 자바의 인덱스 루프와 같은가

1절에서 범위는 세 정수를 든 값이었습니다. 그렇다면 for (i in 0 until n) 을 돌 때마다 그 값을 담은 객체가 하나씩 생길까요. 자바의 for (int i = 0; i < n; i++) 는 정수 변수 하나만 늘리는 반복, 곧 인덱스 루프인데 말입니다. 이 절은 Loops.kt 에서 범위를 쓴 함수를 CFR 로 되돌려 봅니다.

끝이 변수인 1..n 은 while 이 됐다

범위를 쓴 톱레벨 함수(클래스 밖, 파일 바로 아래에 쓴 함수) 가운데 넷입니다. 인자는 n: Int 처럼 이름 뒤에 콜론과 타입을 쓰고, 본문이 한 문장이면 자바처럼 중괄호를 생략합니다. Int 는 자바의 int 입니다.

Kotlin
fun until(n: Int) {
    for (i in 0 until n) println(i)
}

fun closed(n: Int) {
    for (i in 1..n) println(i)
}

fun stepped(n: Int) {
    for (i in 1..n step 2) println(i)
}

fun storedStep(n: Int) {
    val r = 1..n step 2
    for (i in r) println(i)
}
터미널
$ kotlinc Loops.kt -d out
$ java -jar cfr.jar out/LoopsKt.class
...
public final class LoopsKt {
    public static final void until(int n) {
        for (int i = 0; i < n; ++i) {
            System.out.println(i);
        }
    }

    public static final void closed(int n) {
        int i = 1;
        if (i <= n) {
            while (true) {
                System.out.println(i);
                if (i == n) break;
                ++i;
            }
        }
    }
...

맨 위의 머리 주석과 import, 한 줄이 아주 긴 @Metadata(...) 는 ... 로 줄였습니다. @Metadata 는 코틀린 컴파일러가 클래스에 남기는 메타데이터입니다.

until 은 자바에서 손으로 쓰는 for (int i = 0; i < n; ++i) 와 같습니다. 범위 객체는 어디에도 없습니다.

closed 는 모양이 다릅니다. 1..n 도 객체는 없지만, i <= n 을 조건으로 도는 대신 if (i == n) break 로 늘리기 전에 끝값과 견줍니다. 같은 일을 하는 자바 Loops.java 의 closed 는 조건이 i <= n 한 줄인 평범한 for 입니다.

끝이 3 같은 상수면 또 다릅니다. for (i in 1..3) 을 되돌리면 for (int i = 1; i < 4; ++i) 가 나왔습니다. 컴파일러가 <= 3 을 < 4 로 바꿔 쓴 것입니다.

끝값의 종류 컴파일된 모양
상수 3 for (int i = 1; i < 4; ++i)
변수 n while (true) 안에서 if (i == n) break
Int.MAX_VALUE while (true) 안에서 if (i == Integer.MAX_VALUE) break

끝값의 종류가 반복의 모양을 가릅니다. 까닭은 4절에서 봅니다.

step 은 값만 미리 계산하고, 변수에 담으면 객체가 생긴다

같은 출력의 뒷부분, stepped 와 storedStep 입니다.

    public static final void stepped(int n) {
        int i = 1;
        int n2 = ProgressionUtilKt.getProgressionLastElement((int)1, (int)n, (int)2);
        if (i <= n2) {
            while (true) {
                System.out.println(i);
                if (i == n2) break;
                i += 2;
            }
        }
    }
...
    public static final void storedStep(int n) {
        IntProgression r = RangesKt.step((IntProgression)((IntProgression)new IntRange(1, n)), (int)2);
        int i = r.getFirst();
        int n2 = r.getLast();
        int n3 = r.getStep();
...
  • stepped — 객체가 없습니다. 표준 라이브러리 함수 getProgressionLastElement 로 마지막 값만 구하고, 나머지는 closed 와 같은 반복입니다. 1절에서 1..9 step 3 이 7 로 맞춰졌던 그 계산입니다
  • storedStep — new IntRange(1, n) 으로 객체가 생깁니다. RangesKt.step 은 거기에 간격을 붙인 IntProgression 을 돌려줍니다. 반복은 그 객체의 getFirst()·getLast()·getStep() 을 읽고 돕니다. 간격의 부호로 방향이 갈리므로 getStep() 도 따로 읽습니다

나머지 둘도 같게 갈렸습니다. (1..n).reversed() 는 객체 없는 반복이 됐고, val r = 1..n 에 담아 돈 함수에는 new IntRange(1, n) 이 생겼습니다. 이 편에서 돌린 경우로는 in 오른쪽에 범위 식을 바로 썼는가가 객체를 갈랐습니다. 범위를 바로 쓰면 자바의 인덱스 루프처럼 객체 없는 정수 반복이 되고, step 과 reversed() 도 객체를 만들지 않았습니다.

4. 왜 i <= n 으로 안 도나

3절의 closed 는 i <= n 대신 i == n 에서 빠져나갔습니다. 차이는 끝값이 int 의 최댓값일 때 드러납니다. Edge.kt 에서 끝을 Int.MAX_VALUE(자바의 Integer.MAX_VALUE)로 둔 범위를 몇 번 도는지 셌습니다.

Kotlin
fun main() {
    var count = 0
    for (i in Int.MAX_VALUE - 2..Int.MAX_VALUE) count++
    println(count)  // 3
}
터미널
$ kotlin -cp out EdgeKt

자바 쪽은 같은 조건을 for 로 쓰고, 끝나지 않을 경우에 대비해 여섯 번째에 멈추게 했습니다.

Java
int count = 0;
for (int i = Integer.MAX_VALUE - 2; i <= Integer.MAX_VALUE; i++) {
    count++;
    if (count > 5) break;
}
System.out.println(count);  // 6
터미널
$ java -cp jout Edge

코틀린은 딱 세 번 돌았습니다. 자바는 break 에 걸려 6 이 찍혔습니다. 그 break 가 없었다면 끝나지 않습니다.

자바 쪽 — i 가 최댓값에서 1 늘면 int 가 넘쳐 가장 작은 음수가 됩니다. 이렇게 값이 타입의 범위를 벗어나 반대편으로 돌아가는 것을 오버플로라고 합니다. 그래서 i <= Integer.MAX_VALUE 는 영원히 참이고, 반복이 끝나지 않습니다.

코틀린 쪽 — 코틀린 컴파일러가 만든 코드는 늘리기 전에 i == 끝 을 봅니다. 그래서 넘칠 틈이 없습니다.

끝이 상수여도 그것이 Int.MAX_VALUE 면 3절의 1..3 처럼 < 끝 + 1 로 바꿔 쓸 수 없습니다. Int.MAX_VALUE + 1 은 int 에 없는 수라서입니다. 그래서 이 범위를 CFR 로 되돌리면 if (i == Integer.MAX_VALUE) break; 가 나왔습니다.

늘리기 전에 끝값과 견주므로 코틀린의 for 는 int 최댓값에서도 끝납니다.

5. while 과 do-while: 자바와 같아서 짧게

while 과 do-while 은 모양도 뜻도 자바와 같습니다. 이 절은 자바와 갈리는 두 가지만 봅니다. 하나는 do 본문의 변수를 조건에서 읽을 수 있는지, 다른 하나는 조건 안에서 대입할 수 있는지입니다.

do 본문의 변수가 조건에서 보인다

Kotlin
fun main() {
    var n = 3
    while (n > 0) {
        print("$n ")
        n--
    }
    println()
    var tries = 0
    do {
        tries++
        val ok = tries >= 2
    } while (!ok)
    println(tries)
}
터미널
$ kotlinc Retry.kt -d out
$ kotlin -cp out RetryKt
3 2 1 
2
$ javac -d jout Retry.java
Retry.java:7: error: cannot find symbol
        } while (!ok);
                  ^
  symbol:   variable ok
  location: class Retry
1 error
  • var n = 3 — 다시 대입할 수 있는 변수입니다. n-- 는 자바와 같습니다
  • print — 줄바꿈 없이 출력합니다
  • val ok 는 do { } 안에서 선언했는데 바깥 while (!ok) 에서 읽습니다

뒤이어 돌린 자바 Retry.java 도 do { } 안에 boolean ok = tries >= 2; 를 두고 } while (!ok); 로 닫은 것입니다. 자바에서 ok 는 중괄호가 닫히면 사라지는 변수라 조건에서 못 찾았습니다. 코틀린은 do 본문에서 선언한 변수를 while 조건까지 보여 줍니다. 재시도처럼 「해 보고 결과를 조건으로 삼는」 반복에서 바깥 변수를 하나 줄여 줍니다.

조건 안에서 대입할 수 없다

자바에서 줄 단위로 읽을 때 흔히 쓰는 while ((line = reader.readLine()) != null) 을 코틀린 Read.kt 로 옮겼습니다.

터미널
$ kotlinc Read.kt -d out
Read.kt:4:13: error: only expressions are allowed in this context.
    while ((line = reader.readLine()) != null) {
            ^^^^^^^^^^^^^^^^^^^^^^^^

코틀린에서 대입은 값을 내는 식이 아니라서 조건 안에 못 씁니다. 대신 이렇게 씁니다.

Kotlin
fun main() {
    val reader = System.`in`.bufferedReader()
    while (true) {
        val line = reader.readLine() ?: break
        println(line)
    }
}
  • System.`in` — 자바의 System.in 입니다. in 이 코틀린 키워드라 백틱으로 감쌌습니다
  • bufferedReader() — 입력 스트림을 줄 단위로 읽을 수 있는 BufferedReader 로 감싸 주는 표준 라이브러리 함수입니다
  • ?: — 왼쪽 값이 null 이면 오른쪽을 실행하는 연산자입니다. 여기서는 더 읽을 줄이 없으면 break 로 반복을 빠져나갑니다

Read2.kt 로 저장해 printf 'a\nb\n' 을 입력으로 넣었더니 a, b 를 한 줄씩 찍었습니다. 자바 Read.java 도 같은 입력에 같게 찍었습니다.

정리하면, while 과 do-while 은 자바와 같습니다. 다른 점은 do 본문의 변수를 조건에서 읽을 수 있다는 것과, 조건 안에서 대입 대신 ?: break 를 쓴다는 것입니다.

6. break·continue 와 라벨 break@outer

안쪽 반복에서 바깥 반복까지 한 번에 빠져나가려면 자바는 라벨 outer: 를 붙이고 break outer; 를 씁니다. 코틀린은 자바의 콜론 대신 @ 를 씁니다. 선언은 이름 뒤에 붙여 outer@, 부를 때는 이름 앞에 붙여 break@outer 입니다. 이 절은 2중 반복에서 continue 와 break 로 바깥 반복을 건드리는 코드를 두 언어로 돌려 맞대 봅니다.

Kotlin
fun main() {
    outer@ for (i in 1..3) {
        for (j in 1..3) {
            if (j == 2) continue@outer
            if (i == 3) break@outer
            println("$i $j")
        }
    }
    println("end")
}
  • outer@ — 라벨 선언입니다. 이름 뒤에 @ 를 붙여 반복문 앞에 둡니다
  • continue@outer — 안쪽 반복을 버리고 바깥 반복의 다음 차례로 갑니다
  • break@outer — 바깥 반복까지 끝내고 그 뒤의 println("end") 로 갑니다

같은 일을 하는 자바 Find.java 입니다.

Java
outer:
for (int i = 1; i <= 3; i++) {
    for (int j = 1; j <= 3; j++) {
        if (j == 2) continue outer;
        if (i == 3) break outer;
        System.out.println(i + " " + j);
    }
}
System.out.println("end");

두 프로그램 모두 1 1, 2 1, end 를 찍었습니다. i 가 1 과 2 일 때는 j 가 2 에 닿자마자 바깥으로 넘어가 한 줄씩만 찍었습니다. i 가 3 이 되자 첫 j 에서 전부 끝났습니다. 두 언어가 같습니다.

forEach 안의 break

break 가 거절된 경우도 둘 봤습니다.

  • Lambda.kt — 반복문 없이 listOf(1, 2, 3).forEach { } 의 중괄호 안에 if (it == 2) break 를 썼습니다. forEach { } 는 리스트의 원소마다 중괄호 안의 코드를 실행하는 함수이고, it 은 그 원소입니다. 이 break 는 'break' and 'continue' are only allowed inside loops. 로 거절됐습니다
  • NoLabel.kt — 선언하지 않은 라벨로 break@outer 를 쓰면 label does not denote a reachable loop. 가 났습니다

중괄호 안이라서 거절된 것은 아닙니다. 같은 forEach 를 for (x in 1..3) { } 안에 넣고 println("$x $it") 을 붙인 NonLocal.kt 는 컴파일이 됐습니다. 돌리면 1 1 만 찍고 곧장 end 로 갔습니다. x 가 1 일 때 만난 break 가 바깥 for 를 끝낸 것입니다.

이 break 는 바깥 for 를 끝냅니다. 중괄호 밖의 반복까지 끝내는 이 동작은 Kotlin 2.2 에서 안정 기능(Stable)이 됐습니다(코틀린 문서 「What's new in Kotlin 2.2.0」).

정리하면, 라벨 반복은 @ 표기만 다르고 자바와 같게 돕니다. 반복문 밖에서 forEach 에 넘긴 중괄호 안의 break 는 거절되고, 바깥에 for 가 있으면 그 break 는 바깥 for 를 끝냅니다.

7. 한 장 요약

코틀린의 for 는 for (변수 in 무엇) 한 가지뿐이다. 세미콜론 세 칸 대신 ..·..<·until·downTo·step 으로 만든 범위를 넘긴다. in 오른쪽에 범위를 바로 쓰면 객체 없는 정수 반복으로 컴파일된다. 끝이 변수인 1..n 은 i == n 에서 늘리기 전에 빠져나가 int 최댓값에서도 넘치지 않는다.

하려는 일 자바 코틀린 확인한 방법
0 부터 n 앞까지 for (int i = 0; i < n; i++) for (i in 0 until n) · 0..<n CFR 에서 같은 for
1 부터 n 까지 for (int i = 1; i <= n; i++) for (i in 1..n) 끝이 변수일 때 CFR 에서 while + if (i == n) break. 끝이 3 이면 i < 4
끝이 MAX_VALUE 오버플로로 안 끝남 3 번 돌고 끝남 Edge 실행
줄어드는 반복 i-- n downTo 1 · (1..n).reversed(). 5..1 은 빈 범위 실행 · CFR
간격 i += 3 step 3. 마지막 값은 1..7 step 3 처럼 맞춰짐. step 0 은 예외. downTo 의 간격은 -1 Show.kt
범위를 변수에 담기 없음 IntRange·IntProgression 객체가 생김 CFR 의 new IntRange
컬렉션 for (String s : list) for (s in list) CFR
인덱스와 원소 바깥 카운터 indices · withIndex() CFR
do 본문 변수를 조건에서 cannot find symbol 된다 Retry
조건 안 대입 (line = r.readLine()) != null 에러. ?: break 로 씀 Read.kt
라벨 outer: · break outer; outer@ · break@outer Find 실행
forEach { } 안 break 해당 없음 반복문 밖이면 에러, for 안이면 그 for 를 끝냄 Lambda.kt · NonLocal.kt

관련 항목

코틀린에서 반복을 쓰는 문법 요소

for 문 · while 문 · do-while 문 · break 문 · continue 문 · 라벨 · 향상된 for 문

for 문의 in 오른쪽에 오는 반복 대상

범위 · IntRange · IntProgression · 컬렉션 · 배열 · 이터레이터 · Iterable

범위를 만드는 코틀린 연산자와 함수

rangeTo · rangeUntil · until · downTo · step · reversed · 중위 함수

인덱스와 원소를 함께 꺼내는 수단

indices · withIndex · 구조 분해 선언 · 문자열 템플릿

범위와 반복이 부딪히는 경계 값과 예외

정수 오버플로 · Integer.MAX_VALUE · 빈 범위 · IllegalArgumentException

반복문 안에서 코틀린과 자바가 갈리는 문법

대입식 · 엘비스 연산자 · 변수 스코프 · 람다 · forEach

반복문이 클래스 파일이 되어 실행되기까지 거치는 처리 단계

kotlinc · javac · 컴파일러 · 클래스 파일 · JVM · 가상 머신 · 코틀린 표준 라이브러리

클래스 파일을 열어 보는 도구

javap · 디컴파일 · 디컴파일러 · CFR · 메타데이터

이 편에 쓰인 언어와 개발 도구

코틀린 · 자바 · JDK