Tagged union
Tagged union (태그된 유니언)
enum 태그 타입으로 유니언을 선언할 수 있어요. 이렇게 하면 유니언이 태그된(tagged) 유니언이 되는데, 이게 되면 switch 표현식과 함께 쓸 수 있게 됩니다. 스위치에서 태그된 유니언을 분기할 때 태그 값은 추가 캡처로 얻을 수 있어요. 태그된 유니언은 자신의 태그 타입으로 강제(coerce)되기도 하는데, 이 부분은 Type Coercion: Unions and Enums에서 다룹니다.
본문
먼저 태그된 유니언을 선언하고, 그걸 switch에서 써 보는 기본적인 모습부터 볼게요. 태그 타입을 enum으로 정해 주면 유니언은 그 enum의 값으로 '지금 어떤 필드가 활성화된 상태인지'를 기억합니다.
// test_tagged_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const ComplexTypeTag = enum {
ok,
not_ok,
};
const ComplexType = union(ComplexTypeTag) {
ok: u8,
not_ok: void,
};
test "switch on tagged union" {
const c = ComplexType{ .ok = 42 };
try expectEqual(ComplexTypeTag.ok, @as(ComplexTypeTag, c));
switch (c) {
.ok => |value| try expectEqual(42, value),
.not_ok => unreachable,
}
switch (c) {
.ok => |_, tag| {
// Because we're in the '.ok' prong, 'tag' is compile-time known to be '.ok':
comptime std.debug.assert(tag == .ok);
},
.not_ok => unreachable,
}
}
test "get tag type" {
try expectEqual(ComplexTypeTag, std.meta.Tag(ComplexType));
}
$ zig test test_tagged_union.zig
1/2 test_tagged_union.test.switch on tagged union...OK
2/2 test_tagged_union.test.get tag type...OK
All 2 tests passed.
첫 번째 switch에서 .ok 분기 안의 |value|처럼 캡처로 값을 직접 받았죠. 두 번째 switch에서는 |_, tag|처럼 태그만 따로 캡처할 수도 있어요. 이때 .ok 분기 안에 있으니 tag는 컴파일 타임에 .ok임이 보장됩니다. std.meta.Tag(ComplexType)로 유니언의 태그 타입을 꺼내 쓸 수도 있습니다.
스위치 표현에서 태그된 유니언의 페이로드를 수정하고 싶다면, 변수 이름 앞에 *를 붙여서 그 값을 포인터로 받으면 돼요.
// test_switch_modify_tagged_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const ComplexTypeTag = enum {
ok,
not_ok,
};
const ComplexType = union(ComplexTypeTag) {
ok: u8,
not_ok: void,
};
test "modify tagged union in switch" {
var c = ComplexType{ .ok = 42 };
switch (c) {
ComplexTypeTag.ok => |*value| value.* += 1,
ComplexTypeTag.not_ok => unreachable,
}
try expectEqual(43, c.ok);
}
$ zig test test_switch_modify_tagged_union.zig
1/1 test_switch_modify_tagged_union.test.modify tagged union in switch...OK
All 1 tests passed.
|*value|로 받은 뒤 value.* += 1처럼 역참조해서 값을 고쳤죠. 그 결과 c.ok가 43이 된 걸 확인합니다.
이번엔 유니언이 enum 태그 타입을 스스로 추론하게 할 수도 있어요. 또 struct나 enum처럼 유니언에도 메서드를 정의할 수 있습니다.
// test_union_method.zig
const std = @import("std");
const expect = std.testing.expect;
const Variant = union(enum) {
int: i32,
boolean: bool,
// void can be omitted when inferring enum tag type.
none,
fn truthy(self: Variant) bool {
return switch (self) {
Variant.int => |x_int| x_int != 0,
Variant.boolean => |x_bool| x_bool,
Variant.none => false,
};
}
};
test "union method" {
var v1: Variant = .{ .int = 1 };
var v2: Variant = .{ .boolean = false };
var v3: Variant = .none;
try expect(v1.truthy());
try expect(!v2.truthy());
try expect(!v3.truthy());
}
$ zig test test_union_method.zig
1/1 test_union_method.test.union method...OK
All 1 tests passed.
union(enum)이라고 쓰면 태그 타입을 직접 나열하지 않아도 되고, none처럼 타입이 void인 필드는 아예 타입 표기를 생략할 수 있어요. truthy 메서드는 스스로를 switch로 분기해서 각 필드의 진위를 돌려줍니다.
enum 태그 타입을 추론하는 유니언은, 추론된 태그에 **서수값(ordinal value)**을 직접 지정할 수도 있어요. 이때는 태그가 명시적인 정수 타입을 지정해야 합니다. 그리고 활성 필드에 해당하는 서수값을 꺼내고 싶으면 @backingInt를 사용하면 됩니다.
// test_tagged_union_with_tag_values.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const Tagged = union(enum(u32)) {
int: i64 = 123,
boolean: bool = 67,
};
test "tag values" {
const int: Tagged = .{ .int = -40 };
try expectEqual(123, @backingInt(int));
const boolean: Tagged = .{ .boolean = false };
try expectEqual(67, @backingInt(boolean));
}
$ zig test test_tagged_union_with_tag_values.zig
1/1 test_tagged_union_with_tag_values.test.tag values...OK
All 1 tests passed.
int에는 123, boolean에는 67이라는 서수값을 박아 뒀죠. 그런 뒤 @backingInt(int)로 활성 필드의 서수값을 꺼내 원래 지정했던 값과 같은지 확인합니다.
마지막으로, @tagName을 쓰면 필드 이름을 나타내는 comptime [:0]const u8 값을 얻을 수 있습니다.
// test_tagName.zig
const std = @import("std");
const expectEqualSlices = std.testing.expectEqualSlices;
const Small2 = union(enum) {
a: i32,
b: bool,
c: u8,
};
test "@tagName" {
try expectEqualSlices(u8, "a", @tagName(Small2.a));
}
$ zig test test_tagName.zig
1/1 [email protected]
All 1 tests passed.
@tagName(Small2.a)가 문자열 "a"와 일치하는지 확인하고 있어요. 이렇게 하면 필요한 순간에 태그의 이름을 문자열로 꺼내 쓸 수 있습니다.
더 알아보기
- Switch — 태그된 유니언을 분기하는 핵심 문법
- Type Coercion: Unions and Enums — 태그된 유니언이 태그 타입으로 강제되는 규칙
- @backingInt, @tagName — 태그의 서수값·이름을 다루는 내장 함수