기본 필드 값

기본 필드 값 (Default Field Values)

구조체(struct)를 정의할 때, 어떤 필드는 초기화 없이도 값을 갖게 하고 싶을 때가 있어요. Zig는 이럴 때 쓰는 기본 필드 값(default field value) 기능을 제공하는데요, 구조체의 각 필드에 그 필드의 기본값을 나타내는 식(expression)을 붙일 수 있어요.

이 식은 컴파일 타임(comptime)에 실행되기 때문에, 그 필드를 구조체 리터럴에서 생략해도 값이 자동으로 채워져요. 아래 예시를 보면서 익숙해져 볼게요.

출처: Zig Documentation

본문

각 구조체 필드에는 기본 필드 값을 나타내는 식을 붙일 수 있어요. 이런 식은 comptime에 실행되고, 구조체 리터럴 표현식에서 그 필드를 생략할 수 있게 해 줍니다.

const Foo = struct {
    a: i32 = 1234,
    b: i32,
};

test "default struct initialization fields" {
    const x: Foo = .{
        .b = 5,
    };
    if (x.a + x.b != 1239) {
        comptime unreachable;
    }
}
$ zig test struct_default_field_values.zig
1/1 struct_default_field_values.test.default struct initialization fields...OK
All 1 tests passed.

Fooa 필드에는 기본값 1234가 붙어 있어요. 그래서 x를 만들 때 .b = 5만 지정해도 a는 자동으로 1234가 되고, 결과적으로 x.a + x.b1239가 되죠.

잘못된 기본 필드 값 (Faulty Default Field Values)

그런데 기본 필드 값이 항상 좋은 선택인 건 아니에요. 기본 필드 값은 그 필드를 초기화에서 생략해도 구조체의 데이터 불변식(data invariant)이 깨지지 않을 때만 적절해요.

이 기준을 어기면 어떤 일이 벌어지는지, 부적절한 예시를 먼저 볼게요.

const Threshold = struct {
    minimum: f32 = 0.25,
    maximum: f32 = 0.75,

    const Category = enum { low, medium, high };

    fn categorize(t: Threshold, value: f32) Category {
        assert(t.maximum >= t.minimum);
        if (value < t.minimum) return .low;
        if (value > t.maximum) return .high;
        return .medium;
    }
};

pub fn main() !void {
    var threshold: Threshold = .{
        .maximum = 0.20,
    };
    const category = threshold.categorize(0.90);
    std.log.info("category: {t}", .{category});
}

const std = @import("std");
const assert = std.debug.assert;
$ zig build-exe bad_default_value.zig
$ ./bad_default_value
thread 975076 panic: reached unreachable code
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/debug.zig:442:14: 0x1029ea9 in assert (std.zig)
    if (!ok) unreachable; // assertion failure
             ^
/home/ci/work/zig-bootstrap/zig/doc/langref/bad_default_value.zig:8:15: 0x11e8b9a in categorize (bad_default_value.zig)
        assert(t.maximum >= t.minimum);
              ^
/home/ci/work/zig-bootstrap/zig/doc/langref/bad_default_value.zig:19:42: 0x11e762e in main (bad_default_value.zig)
    const category = threshold.categorize(0.90);
                                         ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:788:64: 0x11e7c46 in callMain (std.zig)
    if (fn_info.param_types.len == 0) return wrapMain(root.main());
                                                               ^
/home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:248:5: 0x11e75e1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

이 코드의 문제를 짚어볼게요. Thresholdmaximum >= minimum이라는 불변식을 지켜야 해요. 그런데 main에서 .maximum = 0.20만 지정하고 minimum은 기본값 0.25를 그대로 쓰게 됐죠. 그 결과 maximum(0.20) < minimum(0.25)라는 불변식 위반이 생기고, categorizeassert가 실패해서 크래시가 나요. 기본 필드 값이 오히려 불변식을 깨뜨린 거예요.

이 원칙을 무시하면 위험하다는 걸 알 수 있죠. 기본 필드 값이 데이터 불변식을 위반하게 만들어, 잘못된 동작(illegal behavior)을 일으켰어요.

이걸 고치려면 모든 필드에서 기본값을 제거하고, **이름 있는 기본값(named default value)**을 제공하면 돼요.

const Threshold = struct {
    minimum: f32,
    maximum: f32,

    const default: Threshold = .{
        .minimum = 0.25,
        .maximum = 0.75,
    };
};

이렇게 하면 default라는 이름이 붙은 상수 하나가 기본 구성을 대표하고, 개발자가 명시적으로 Threshold.default를 쓸 때만 그 값이 적용돼요. 불변식은 항상 명시적 초기화로부터 지켜지게 되죠.

마지막으로 한 가지 더 알아둘 점이 있어요. 구조체 값이 불변식을 위반하지 않고 초기화되려면 런타임(runtime)에 알려진 값이 필요한 경우도 있어요. 이럴 땐 기본값에 의존하지 말고, 그런 런타임 값을 인자로 받아 나머지 필드를 채워주는 **초기화 메서드(initialization method)**를 쓰는 게 맞아요.

더 알아보기

  • comptime — 컴파일 타임 평가, 기본 필드 값이 실행되는 시점
  • Struct — 구조체 정의와 필드
  • struct literal — 구조체 리터럴에서 필드 생략