할당

할당 (Assignment)

Zig에서 값을 식별자(identifier)에 붙이는 방법, 즉 변수에 값을 담는 방법을 다루는 섹션이에요. 여기서는 값이 한번 정해지면 바뀌지 않는 const와, 언제든 바꿀 수 있는 var, 그리고 값을 나눠 담는 destructuring까지 하나씩 짚어볼게요.

출처: Zig Documentation

본문

const: 값이 바뀌지 않는 식별자

const 키워드를 쓰면 어떤 식별자에 값을 할당할 수 있어요. 파일 스코프에서도, 함수 안에서도 똑같이 쓸 수 있죠.

const x = 1234;

fn foo() void {
    // It works at file scope as well as inside functions.
    const y = 5678;

    // Once assigned, an identifier cannot be changed.
    y += 1;
}

pub fn main() void {
    foo();
}

이 코드를 컴파일하면, 함수 안에서 y에 값을 다시 더하려는 순간 오류가 나요. 한번 할당된 식별자는 바꿀 수 없기 때문이에요.

$ zig build-exe constant_identifier_cannot_change.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/constant_identifier_cannot_change.zig:8:5: error: cannot assign to constant
    y += 1;
    ^
referenced by:
    main: /home/ci/work/zig-bootstrap/zig/doc/langref/constant_identifier_cannot_change.zig:12:8
    callMain [inlined]: /home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:788:64
    callMainWithArgs [inlined]: /home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:729:20
    posixCallMainAndExit: /home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:681:38
    2 reference(s) hidden; use '-freference-trace=6' to see all references

여기서 짚고 넘어갈 점이 하나 있어요. const는 식별자가 바로 가리키는 모든 바이트에 적용돼요. Pointers는 그 자체로만의 const 여부(pointer const-ness)를 따로 가지니까, 이건 나중에 포인터를 다룰 때 다시 이야기할게요.

var: 바꿀 수 있는 변수

값을 바꿔야 할 때는 var 키워드를 써요.

const print = @import("std").debug.print;

pub fn main() void {
    var y: i32 = 5678;

    y += 1;

    print("{d}", .{y});
}
$ zig build-exe mutable_var.zig
$ ./mutable_var
5679

yvar로 선언했으니 y += 1이 문제없이 실행되고, 출력에도 5679가 찍히는 걸 확인할 수 있어요.

변수는 반드시 초기화해야 해요

var는 값이 오는 자리에 초기화 값이 반드시 있어야 해요. 빈 선언만 하고 나중에 값을 넣으려 하면 컴파일 오류가 나요.

pub fn main() void {
    var x: i32;

    x = 1;
}
$ zig build-exe var_must_be_initialized.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/var_must_be_initialized.zig:2:15: error: expected '=', found ';'
    var x: i32;
              ^

var x: i32;처럼 타입만 쓰고 값을 안 넣으면, 컴파일러가 기대하는 = 대신 ;를 만났다고 알려줘요. 그런데 꼭 미리 값을 알고 있어야만 초기화할 수 있는 건 아니에요. undefined를 쓰면 "아직 정하지 않았다"는 뜻으로 선언해 둘 수 있어요. 이건 다음 소제목에서 자세히 볼게요.

undefined: 일부러 초기화를 미루기

undefined를 쓰면 변수를 초기화 없이 두고, 나중에 값을 넣을 때까지 기다릴 수 있어요.

const print = @import("std").debug.print;

pub fn main() void {
    var x: i32 = undefined;
    x = 1;
    print("{d}", .{x});
}
$ zig build-exe assign_undefined.zig
$ ./assign_undefined
1

여기서 xundefined로 시작했다가 x = 1;에서 값이 채워져요. 이럴 때 쓸 수 있는 게 undefined예요.

undefined는 어느 타입으로든 coerced될 수 있어요. 다만 한번 그렇게 변환되고 나면, 그 값이 undefined였다는 사실은 더 이상 알아낼 수 없어요. undefined라는 건 "값이 뭐든 될 수 있다"는 뜻이고, 그 값이 타입 기준으로 볼 때 말도 안 되는 값일 수도 있어요. 영어로 풀어 쓰면 "의미 있는 값이 아님(Not a meaningful value)"이라는 뜻이에요. "이 값을 쓰는 건 버그다. 이 값은 쓰이지 않거나, 쓰이기 전에 덮어써질 값"이라는 의미죠.

또 하나 알아둘 점이 있어요. Debug 모드와 ReleaseSafe 모드에서는 Zig가 undefined 메모리에 0xaa 바이트를 써 넣어요. 버그를 일찍 잡고, 디버거에서 undefined 메모리가 쓰인 흔적을 발견하기 쉽게 하기 위해서예요. 다만 이건 언어의 의미론(semantic)이 아니라 구현상의 동작이라, 코드에서 항상 관찰된다고 보장되지는 않아요.

Destructuring: 값을 나눠 담기

destructuring 할당은 인덱스로 접근 가능한 집합 타입(aggregate) — Tuples, Arrays, Vectors — 의 요소들을 각각 분리해서 담을 수 있게 해줘요.

const print = @import("std").debug.print;

pub fn main() void {
    var x: u32 = undefined;
    var y: u32 = undefined;
    var z: u32 = undefined;

    const tuple = .{ 1, 2, 3 };

    x, y, z = tuple;

    print("tuple: x = {}, y = {}, z = {}\n", .{x, y, z});

    const array = [_]u32{ 4, 5, 6 };

    x, y, z = array;

    print("array: x = {}, y = {}, z = {}\n", .{x, y, z});

    const vector: @Vector(3, u32) = .{ 7, 8, 9 };

    x, y, z = vector;

    print("vector: x = {}, y = {}, z = {}\n", .{x, y, z});
}
$ zig build-exe destructuring_to_existing.zig
$ ./destructuring_to_existing
tuple: x = 1, y = 2, z = 3
array: x = 4, y = 5, z = 6
vector: x = 7, y = 8, z = 9

x, y, z = tuple;처럼 왼쪽에 여러 식별자를 콤마로 나열하면, 오른쪽 값의 첫 요소부터 차례로 담겨요. 튜플, 배열, 벡터 모두 같은 방식으로 동작해요.

여기에 몇 가지 규칙이 있어요. destructuring 표현식은 블록 안에서만 쓸 수 있고, Namespace 스코프에서는 쓸 수 없어요. 그리고 할당의 왼쪽은 콤마로 구분된 목록이어야 하는데, 각 요소는 기존 var 같은 lvalue이거나 변수 선언이어야 해요.

const print = @import("std").debug.print;

pub fn main() void {
    var x: u32 = undefined;

    const tuple = .{ 1, 2, 3 };

    x, var y : u32, const z = tuple;

    print("x = {}, y = {}, z = {}\n", .{x, y, z});

    // y is mutable
    y = 100;

    // You can use _ to throw away unwanted values.
    _, x, _ = tuple;

    print("x = {}\n", .{x});
}
$ zig build-exe destructuring_mixed.zig
$ ./destructuring_mixed
x = 1, y = 2, z = 3
x = 2

이 예시를 보면 재미있는 점이 보여요. 왼쪽에서 기존 x는 그대로 두고, var y : u32, const z처럼 새 변수를 선언하면서 동시에 값을 받을 수도 있어요. yvar라서 이후에 y = 100;처럼 바꿀 수 있죠. 그리고 값이 필요 없는 자리는 _로 버릴 수 있어요. 마지막에 _, x, _ = tuple;로 다시 x에 두 번째 요소인 2를 담았죠.

destructuring은 comptime 키워드를 앞에 붙일 수도 있어요. 그러면 destructure 표현식 전체가 comptime에 평가돼요. 이때 선언되는 모든 varcomptime var가 되고, 모든 표현식 — 결과 위치(result location)와 assignee 표현식 모두 — 이 comptime에 평가돼요.

더 알아보기

destructuring은 다루는 타입별로 더 자세한 내용이 따로 있어요. 튜플·배열·벡터 각각의 케이스를 보고 싶다면 아래 링크를 따라가 보세요.