for 반복문

for 반복문 (for)

for는 슬라이스(slice)나 배열 같은 순회 가능한 것들을 차례로 훑을 때 쓰는 반복문이에요. while처럼 breakcontinue, 그리고 else도 함께 쓸 수 있어서, "이 값들을 하나씩 보면서 뭔가를 해야 한다"는 상황에서 가장 익숙하고 자연스러운 도구예요. 아래 예시에서 for가 동작하는 기본 모습을 확인해 봐요.

const expectEqual = @import("std").testing.expectEqual;

test "for basics" {
    const items = [_]i32{ 4, 5, 3, 4, 0 };
    var sum: i32 = 0;

    // For loops iterate over slices and arrays.
    for (items) |value| {
        // Break and continue are supported.
        if (value == 0) {
            continue;
        }
        sum += value;
    }
    try expectEqual(16, sum);

    // To iterate over a portion of a slice, reslice.
    for (items[0..1]) |value| {
        sum += value;
    }
    try expectEqual(20, sum);

    // To access the index of iteration, specify a second condition as well
    // as a second capture value.
    var sum2: i32 = 0;
    for (items, 0..) |_, i| {
        try expectEqual(usize, @TypeOf(i));
        sum2 += @as(i32, @intCast(i));
    }
    try expectEqual(10, sum2);

    // To iterate over consecutive integers, use the range syntax.
    // Unbounded range is always a compile error.
    var sum3: usize = 0;
    for (0..5) |i| {
        sum3 += i;
    }
    try expectEqual(10, sum3);
}

test "multi object for" {
    const items = [_]usize{ 1, 2, 3 };
    const items2 = [_]usize{ 4, 5, 6 };
    var count: usize = 0;

    // Iterate over multiple objects.
    // All lengths must be equal at the start of the loop, otherwise detectable
    // illegal behavior occurs.
    for (items, items2) |i, j| {
        count += i + j;
    }

    try expectEqual(21, count);
}

test "for reference" {
    var items = [_]i32{ 3, 4, 2 };

    // Iterate over the slice by reference by
    // specifying that the capture value is a pointer.
    for (&items) |*value| {
        value.* += 1;
    }

    try expectEqual(4, items[0]);
    try expectEqual(5, items[1]);
    try expectEqual(3, items[2]);
}

test "for else" {
    // For allows an else attached to it, the same as a while loop.
    const items = [_]?i32{ 3, 4, null, 5 };

    // For loops can also be used as expressions.
    // Similar to while loops, when you break from a for loop, the else branch is not evaluated.
    var sum: i32 = 0;
    const result = for (items) |value| {
        if (value != null) {
            sum += value.?;
        }
    } else blk: {
        try expectEqual(12, sum);
        break :blk sum;
    };
    try expectEqual(12, result);
}
$ zig test test_for.zig
1/4 test_for.test.for basics...OK
2/4 test_for.test.multi object for...OK
3/4 test_for.test.for reference...OK
4/4 test_for.test.for else...OK
All 4 tests passed.

출처: Zig Documentation

본문

for 반복문은 기본적으로 슬라이스와 배열을 순회해요. 몇 가지 쓰임새를 천천히 짚어 볼게요.

  • 부분 순회: 슬라이스의 일부만 보고 싶다면, 먼저 다시 슬라이스(reslice)를 만들어서 순회하면 돼요. items[0..1]처럼 범위를 잘라서 넘기는 식이에요.
  • 인덱스 함께 얻기: 순회 대상 뒤에 0..(끝이 없는 범위)을 하나 더 주면, 반복 횟수를 세는 두 번째 캡처 값을 받을 수 있어요. for (items, 0..) |_, i|에서 i가 바로 그 인덱스예요.
  • 연속된 정수 순회: for (0..5) |i|처럼 범위(range) 문법을 쓰면 0부터 4까지 정수를 차례로 순회해요. 주의할 점은, 끝이 정해지지 않은 범위(unbounded range)를 쓰면 항상 컴파일 오류가 난다는 거예요.
  • 여러 객체 동시 순회: for (items, items2) |i, j|처럼 피연산자를 콤마로 나열하면 여러 객체를 한 번에 순회할 수 있어요. 이때 모든 객체의 길이는 반복이 시작되는 시점에 서로 같아야 해요. 길이가 다르면 감지 가능한 불법 동작(detectable illegal behavior)이 발생해요.
  • 참조로 순회: 캡처 값을 포인터 타입으로 받고 싶으면 for (&items) |*value|처럼 쓰면 돼요. 이렇게 하면 value.*로 원본 요소를 직접 수정할 수 있어요.
  • else 절: while과 마찬가지로 for에도 else를 붙일 수 있어요. 그리고 for는 표현식(expression)으로도 쓸 수 있어요. while과 비슷하게, for 루프에서 break로 빠져나오면 else 분기는 평가되지 않아요. 아래 예시처럼 라벨이 붙은 블록(blk:)과 break :blk을 조합해 루프 전체가 값을 만들어 내도록 만들 수도 있어요.
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "nested break" {
    var count: usize = 0;
    outer: for (1..6) |_| {
        for (1..6) |_| {
            count += 1;
            break :outer;
        }
    }
    try expectEqual(1, count);
}

test "nested continue" {
    var count: usize = 0;
    outer: for (1..9) |_| {
        for (1..6) |_| {
            count += 1;
            continue :outer;
        }
    }

    try expectEqual(8, count);
}
$ zig test test_for_nested_break.zig
1/2 test_for_nested_break.test.nested break...OK
2/2 test_for_nested_break.test.nested continue...OK
All 2 tests passed.

라벨이 붙은 for (Labeled for)

for 루프에 라벨을 붙이면, 그 라벨을 써서 중첩된 안쪽 루프에서 breakcontinue로 바깥 루프를 제어할 수 있어요. 위 예시의 outer:가 바로 라벨이고, break :outer;는 안쪽 루프에서도 곧바로 바깥 루프를 통째로 빠져나가게 해줘요. continue :outer;는 바깥 루프의 다음 반복으로 건너뛰게 하죠. 루프가 여러 겹일 때 특정 루프를 정확히 겨냥해야 한다면 이 라벨을 기억해 두면 좋아요.

inline for

for 루프는 inline으로 만들 수 있어요. inline for로 선언하면 루프가 언롤(unroll)되어 펼쳐져서, 컴파일 타임에만 가능한 일(subtle)들을 할 수 있게 돼요. 예를 들어 타입(type)을 first-class 값처럼 다루는 코드가 그런 경우예요. inline for의 캡처 값과 반복자 값은 컴파일 타임에 이미 알려진 값이 돼요.

const expectEqual = @import("std").testing.expectEqual;

test "inline for loop" {
    const nums = [_]i32{ 2, 4, 6 };
    var sum: usize = 0;
    inline for (nums) |i| {
        const T = switch (i) {
            2 => f32,
            4 => i8,
            6 => bool,
            else => unreachable,
        };
        sum += typeNameLength(T);
    }
    try expectEqual(9, sum);
}

fn typeNameLength(comptime T: type) usize {
    return @typeName(T).len;
}
$ zig test test_inline_for.zig
1/1 test_inline_for.test.inline for loop...OK
All 1 tests passed.

inline for다음 두 가지 이유가 있을 때만 쓰는 걸 추천해요.

  • 루프가 comptime에 실행되어야 의미가 성립하는 경우
  • 이렇게 강제로 언롤하는 것이 실제로 더 빠르다는 걸 벤치마크로 증명할 수 있는 경우

달리 필요한 이유가 없다면 평범한 for로 충분해요. inline은 이름 그대로 "컴파일 시점에 펼쳐라"는 뜻이라, 남용하면 컴파일 결과가 오히려 커지거나 복잡해질 수 있으니 신중하게 쓰는 게 좋아요.

더 알아보기 (Learn more)

for와 나란히 쓰이는 개념을 이어서 보면 전체 흐름이 잘 잡혀요.

  • while — 조건 기반 반복문. for와 짝을 이루는 또 다른 반복문이에요.
  • comptimeinline for가 컴파일 타임에 동작하는 원리를 이해하는 데 필요해요.
  • Arraysfor의 순회 대상인 배열의 기본 구조예요.
  • Slices — 배열과 함께 for가 가장 자주 순회하는 슬라이스예요.