기본 필드 값
기본 필드 값 (Default Field Values)
구조체(struct)를 정의할 때, 어떤 필드는 초기화 없이도 값을 갖게 하고 싶을 때가 있어요. Zig는 이럴 때 쓰는 기본 필드 값(default field value) 기능을 제공하는데요, 구조체의 각 필드에 그 필드의 기본값을 나타내는 식(expression)을 붙일 수 있어요.
이 식은 컴파일 타임(comptime)에 실행되기 때문에, 그 필드를 구조체 리터럴에서 생략해도 값이 자동으로 채워져요. 아래 예시를 보면서 익숙해져 볼게요.
본문
각 구조체 필드에는 기본 필드 값을 나타내는 식을 붙일 수 있어요. 이런 식은 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.
Foo의 a 필드에는 기본값 1234가 붙어 있어요. 그래서 x를 만들 때 .b = 5만 지정해도 a는 자동으로 1234가 되고, 결과적으로 x.a + x.b는 1239가 되죠.
잘못된 기본 필드 값 (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)
이 코드의 문제를 짚어볼게요. Threshold는 maximum >= minimum이라는 불변식을 지켜야 해요. 그런데 main에서 .maximum = 0.20만 지정하고 minimum은 기본값 0.25를 그대로 쓰게 됐죠. 그 결과 maximum(0.20) < minimum(0.25)라는 불변식 위반이 생기고, categorize의 assert가 실패해서 크래시가 나요. 기본 필드 값이 오히려 불변식을 깨뜨린 거예요.
이 원칙을 무시하면 위험하다는 걸 알 수 있죠. 기본 필드 값이 데이터 불변식을 위반하게 만들어, 잘못된 동작(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 — 구조체 리터럴에서 필드 생략