for 반복문
for 반복문 (for)
for는 슬라이스(slice)나 배열 같은 순회 가능한 것들을 차례로 훑을 때 쓰는 반복문이에요. while처럼 break와 continue, 그리고 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.
본문
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 루프에 라벨을 붙이면, 그 라벨을 써서 중첩된 안쪽 루프에서 break나 continue로 바깥 루프를 제어할 수 있어요. 위 예시의 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와 나란히 쓰이는 개념을 이어서 보면 전체 흐름이 잘 잡혀요.