배열
배열 (Arrays)
배열은 같은 타입의 값들을 고정된 개수만큼 나란히 묶어 놓은 타입이에요. 여기서 고정이라는 말이 핵심인데, 배열의 길이는 컴파일 타임에 정해져 있어서 런타임에 바꿀 수 없어요. 길이를 나중에 바꿔야 한다면 그건 배열이 아니라 슬라이스(slices)가 할 일이에요. 이 구간에서는 배열을 어떻게 만들고, 읽고, 초기화하고, 또 구조 분해 하는지 하나씩 살펴볼게요.
본문
배열 리터럴은 [_]T{ ... } 형태로 써요. 길이 자리([ ] 안)에 밑줄 _을 넣으면, Zig가 요소의 개수를 보고 길이를 알아서 추론해 줘요. 아래 코드의 message가 그 예인데, 원소 다섯 개를 넣었으니 [5]u8 배열이 되는 거예요. 그리고 [5]u8처럼 길이를 직접 명시하고 .{} 형태로 초기화하는 것도 같은 결과예요. 이건 결과 위치(result location)를 이용한 초기화 방식이에요.
const expectEqual = @import("std").testing.expectEqual;
const assert = @import("std").debug.assert;
const mem = @import("std").mem;
// array literal
const message = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
// alternative initialization using result location
const alt_message: [5]u8 = .{ 'h', 'e', 'l', 'l', 'o' };
comptime {
assert(mem.eql(u8, &message, &alt_message));
}
// get the size of an array
comptime {
assert(message.len == 5);
}
// A string literal is a single-item pointer to an array.
const same_message = "hello";
comptime {
assert(mem.eql(u8, &message, same_message));
}
test "iterate over an array" {
var sum: usize = 0;
for (message) |byte| {
sum += byte;
}
try expectEqual('h' + 'e' + 'l' * 2 + 'o', sum);
}
// modifiable array
var some_integers: [100]i32 = undefined;
test "modify an array" {
for (&some_integers, 0..) |*item, i| {
item.* = @intCast(i);
}
try expectEqual(10, some_integers[10]);
try expectEqual(99, some_integers[99]);
}
// array concatenation works if the values are known
// at compile time
const part_one = [_]i32{ 1, 2, 3, 4 };
const part_two = [_]i32{ 5, 6, 7, 8 };
const all_of_it = part_one ++ part_two;
comptime {
assert(mem.eql(i32, &all_of_it, &[_]i32{ 1, 2, 3, 4, 5, 6, 7, 8 }));
}
// remember that string literals are arrays
const hello = "hello";
const world = "world";
const hello_world = hello ++ " " ++ world;
comptime {
assert(mem.eql(u8, hello_world, "hello world"));
}
// initialize an array to zero
const all_zero: [10]u16 = @splat(0);
comptime {
assert(all_zero.len == 10);
assert(all_zero[5] == 0);
}
// use compile-time code to initialize an array
var fancy_array = init: {
var initial_value: [10]Point = undefined;
for (&initial_value, 0..) |*pt, i| {
pt.* = Point{
.x = @intCast(i),
.y = @intCast(i * 2),
};
}
break :init initial_value;
};
const Point = struct {
x: i32,
y: i32,
};
test "compile-time array initialization" {
try expectEqual(4, fancy_array[4].x);
try expectEqual(8, fancy_array[4].y);
}
// call a function to initialize an array
var more_points: [10]Point = @splat(makePoint(3));
fn makePoint(x: i32) Point {
return Point{
.x = x,
.y = x * 2,
};
}
test "array initialization with function calls" {
try expectEqual(3, more_points[4].x);
try expectEqual(6, more_points[4].y);
try expectEqual(10, more_points.len);
}
$ zig test test_arrays.zig
1/4 test_arrays.test.iterate over an array...OK
2/4 test_arrays.test.modify an array...OK
3/4 test_arrays.test.compile-time array initialization...OK
4/4 test_arrays.test.array initialization with function calls...OK
All 4 tests passed.
코드에서 몇 가지만 짚고 넘어갈게요. message.len을 보면 배열의 길이를 묻는건데, 이 값은 comptime assert 안에서 확인되니까 컴파일 타임에 이미 정해져 있는 값이에요. 또 same_message = "hello"라는 문자열 리터럴이 message와 mem.eql로 비교되는 걸 볼 수 있는데, 그래서 "문자열 리터럴은 배열을 가리키는 single-item 포인터"라는 주석이 붙어 있는 거예요.
배열을 바꾸고 싶다면 var로 선언하면 돼요. some_integers가 undefined로 시작해서 for 루프가 요소마다 값을 채워 넣죠. 여기 0..은 무한히 증가하는 정수 범위라서 i가 각 인덱스가 되고, @intCast(i)로 알맞은 타입에 맞춰 저장하고 있어요.
배열끼리 ++ 연산자로 **이어 붙이기(concatenation)**도 가능해요. 다만 이건 두 배열의 값이 컴파일 타임에 알려져 있을 때만 동작해요. part_one ++ part_two는 값이 다 정해져 있으니 하나의 배열로 합쳐지고, hello ++ " " ++ world처럼 문자열(곧 문자 배열)도 마찬가지로 이어 붙일 수 있어요.
마지막으로 초기화 방법이 세 가지 더 나와요. @splat(0)은 배열 전체를 0으로 채워 줘요. 그리고 라벨이 붙은 블록(init: { ... break :init ... })을 쓰면 컴파일 타임 코드로 각 요소를 계산해서 배열을 초기화할 수 있고, @splat(makePoint(3))처럼 함수를 호출해서 배열을 채우는 방법도 있어요.
다차원 배열 (Multidimensional Arrays)
배열을 배열 안에 중첩하면 다차원 배열을 만들 수 있어요. 아래 mat4x5는 [4][5]f32 타입인데, 겉으로는 4행 5열짜리 2차원 행렬처럼 보이지만 사실은 배열 4개를 담고 있는 1차원 배열이에요.
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const mat4x5 = [4][5]f32{
[_]f32{ 1.0, 0.0, 0.0, 0.0, 0.0 },
[_]f32{ 0.0, 1.0, 0.0, 1.0, 0.0 },
[_]f32{ 0.0, 0.0, 1.0, 0.0, 0.0 },
[_]f32{ 0.0, 0.0, 0.0, 1.0, 9.9 },
};
test "multidimensional arrays" {
// mat4x5 itself is a one-dimensional array of arrays.
try expectEqual(mat4x5[1], [_]f32{ 0.0, 1.0, 0.0, 1.0, 0.0 });
// Access the 2D array by indexing the outer array, and then the inner array.
try expectEqual(9.9, mat4x5[3][4]);
// Here we iterate with for loops.
for (mat4x5, 0..) |row, row_index| {
for (row, 0..) |cell, column_index| {
if (row_index == column_index) {
try expectEqual(1.0, cell);
}
}
}
// Initialize a multidimensional array to zeros.
const all_zero: [4][5]f32 = @splat(@splat(0));
try expectEqual(0, all_zero[0][0]);
}
$ zig test test_multidimensional_arrays.zig
1/1 test_multidimensional_arrays.test.multidimensional arrays...OK
All 1 tests passed.
mat4x5[1]처럼 바깥 배열을 인덱싱하면 안쪽 배열 하나가 통째로 나오고, mat4x5[3][4]처럼 두 번 연속으로 인덱싱하면 특정 요소 하나에 접근해요. for 루프를 두 번 겹치면 행과 열로 순회할 수 있는데, 위 예제는 대각선(row_index == column_index)에 있는 요소만 1.0인지 확인하고 있죠. 참고로 다차원 배열도 @splat을 겹쳐서 0으로 초기화할 수 있어요.
센티널 종료 배열 (Sentinel-Terminated Arrays)
[N:x]T 문법은 센티널(sentinel) 요소를 가진 배열을 가리켜요. 설명만으론 낯설 수 있는데, 뒤에 나올 예시 코드를 보면 바로 이해가 돼요.
The syntax
[N:x]Tdescribes an array which has a sentinel element of valuexat the index corresponding to the lengthN.
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test "0-terminated sentinel array" {
const array = [_:0]u8{ 1, 2, 3, 4 };
try expectEqual([4:0]u8, @TypeOf(array));
try expectEqual(4, array.len);
try expectEqual(0, array[4]);
}
test "extra 0s in 0-terminated sentinel array" {
// The sentinel value may appear earlier, but does not influence the compile-time 'len'.
const array = [_:0]u8{ 1, 0, 0, 4 };
try expectEqual([4:0]u8, @TypeOf(array));
try expectEqual(4, array.len);
try expectEqual(0, array[4]);
}
$ zig test test_null_terminated_array.zig
1/2 test_null_terminated_array.test.0-terminated sentinel array...OK
2/2 test_null_terminated_array.test.extra 0s in 0-terminated sentinel array...OK
All 2 tests passed.
[_:0]u8{ 1, 2, 3, 4 }는 길이가 4인 u8 배열인데, 인덱스 4 자리에 값 0이 추가로 붙는 타입이라서 결과 타입이 [4:0]u8이 돼요. 그래서 array[4]에 접근하면 0이 나오죠. 여기서 흥미로운 점은, 센티널 값이 배열 안쪽에 먼저 등장해도({ 1, 0, 0, 4 }처럼) 컴파일 타임의 len에는 아무 영향이 없다는 거예요. len은 여전히 4고, 인덱스 4의 센티널만 0으로 접근 가능한 거죠.
배열 구조 분해 (Destructuring Arrays)
배열은 **구조 분해(destructuring)**할 수 있어요. 구조 분해는 배열을 한 번에 여러 변수로 풀어내는 문법인데, 아래 예제가 딱 그걸 보여줘요. rgba 배열의 네 요소를 r, g, b, a라는 이름으로 한 번에 꺼내서 순서를 뒤바꿔 새 배열을 만들고 있어요.
const print = @import("std").debug.print;
fn swizzleRgbaToBgra(rgba: [4]u8) [4]u8 {
// readable swizzling by destructuring
const r, const g, const b, const a = rgba;
return .{ b, g, r, a };
}
pub fn main() void {
const pos = [_]i32{ 1, 2 };
const x, const y = pos;
print("x = {}, y = {}\n", .{x, y});
const orange: [4]u8 = .{ 255, 165, 0, 255 };
print("{any}\n", .{swizzleRgbaToBgra(orange)});
}
$ zig build-exe destructuring_arrays.zig
$ ./destructuring_arrays
x = 1, y = 2
{ 0, 165, 255, 255 }
const r, const g, const b, const a = rgba;가 구조 분해예요. 배열 rgba의 요소 순서대로 각 변수에 값이 들어가고, return .{ b, g, r, a };로 순서를 바꿔서 돌려주죠. pos도 { 1, 2 }라서 x = 1, y = 2로 분해돼요. 실행 결과를 보면 orange { 255, 165, 0, 255 }가 들어가서 { 0, 165, 255, 255 }로 나오는데, r(255)과 b(0)의 자리가 바뀐 걸 확인할 수 있어요.
더 알아보기
- for — 배열 요소를 순회하는 루프
- Slices — 길이가 런타임에 정해지는 연속된 요소 묶음
- Sentinel-Terminated Pointers, Sentinel-Terminated Slices — 센티널 개념이 포인터·슬라이스에서 어떻게 쓰이는지
- Destructuring, Destructuring Tuples, Destructuring Vectors — 구조 분해가 다른 타입에서 어떻게 동작하는지