정수와 enum 사이의 형 변환
정수와 enum 사이의 형 변환 (IntCastEnum)
enum은 겉보기엔 이름들의 집합이라 정수랑 전혀 안 어울릴 것 같죠? 그런데 사실 enum은 정수 태그 타입(integer tag type)을 하나 숨기고 있어서, 정수 값과 서로 오갈 수 있어요. 이게 가능해야 하는 이유가 있어요. enum 값은 결국 메모리에서 특정 정수로 저장되니까요. 오늘은 이 enum ↔ 정수 변환을 Zig에서 어떻게 하는지 살펴볼게요.
본문
enum은 정수 태그 타입을 갖는다
enum을 그냥 만들면, 컴파일러가 태그 타입을 스스로 추론해요. 하지만 폭을 직접 지정하면 정수와의 관계가 훨씬 명확해집니다. 아래처럼 enum(u2)라고 쓰면 u2와 Value 사이를 바로 형 변환할 수 있어요. 순서 값(ordinal value)은 0부터 시작해서 이전 항목보다 1씩 커지는 게 기본이에요.
// If you want access to the ordinal value of an enum, you
// can specify the tag type.
const Value = enum(u2) {
zero,
one,
two,
};
// Now you can cast between u2 and Value.
// The ordinal value starts from 0, counting up by 1 from the previous member.
test "enum ordinal value" {
try expectEqual(0, @backingInt(Value.zero));
try expectEqual(1, @backingInt(Value.one));
try expectEqual(2, @backingInt(Value.two));
}
u2는 값 두 개를 담을 수 있는 2비트 정수이고, Value에는 항목이 세 개(zero, one, two) 있으니 0~2의 값을 정확히 담을 수 있죠. 그래서 이 둘 사이 변환은 안전하게 성립해요.
enum → 정수: @intFromEnum
enum 값을 정수로 바꾸려면 @intFromEnum을 써요. 프로토타입은 이렇게 생겼어요.
@intFromEnum(enum_or_tagged_union: anytype) anytype
enum 값을 그 정수 태그 타입으로 바꿔 줍니다. 받는 쪽에 tagged union을 넘기면, 그 유니온의 태그 값을 enum 값처럼 사용해 정수로 바꿔요. 예를 들어 앞서 본 Value라면 @intFromEnum(Value.two)가 정수 2를 반환하는 식이에요.
한 가지 짚어둘게요. 이 함수는 지금은 deprecated(더 이상 권장되지 않는) 상태예요. 새 코드에서는 대신 @backingInt나 @bitCast를 쓰는 걸 권장해요.
정수 → enum: @enumFromInt
반대 방향인 정수 → enum은 @enumFromInt가 담당해요.
@enumFromInt(integer: anytype) anytype
정수를 enum 값으로 바꿔 주고, 반환 타입은 추론된 결과 타입(inferred result type)으로 정해져요.
여기서 꼭 알아둬야 할 동작이 있어요. enum에 대응하는 값이 없는 정수를 변환하려고 하면 safety-checked Illegal Behavior(안전 검사로 잡아내는 잘못된 동작)이 발동해요. 다만 non-exhaustive enum(비완전 열거형)은 예외인데요, 이런 enum은 자기 정수 태그 타입에 들어 있는 모든 정수에 대응하는 값을 가지기 때문이에요. 이때 _ 값이 이름 없는 나머지 정수들을 전부 대표해 줍니다.
@enumFromInt 역시 지금은 deprecated예요. 새 코드에서는 @fromBackingInt나 @bitCast를 쓰는 걸 권장해요.
잘못된 enum cast는 어떻게 될까 (Invalid Enum Cast)
그럼 실제로 대응 값이 없는 정수를 enum으로 캐스팅하면 어떤 일이 벌어질까요? 컴파일 타임과 런타임에서 각각 다르게 나타나요. 먼저 컴파일 타임부터 볼게요.
const Foo = enum(u2) {
a,
b,
c,
};
comptime {
const a: u2 = 3;
const b: Foo = @fromBackingInt(a);
_ = b;
}
u2는 03을 담을 수 있는데 2)만 있으니, 값 3에 대응하는 enum이 없어요. 그래서 컴파일러가 오류를 냅니다.Foo에는 a, b, c(0
$ zig test test_comptime_invalid_enum_cast.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_invalid_enum_cast.zig:8:20: error: enum 'test_comptime_invalid_enum_cast.Foo' has no tag with value '3'
const b: Foo = @fromBackingInt(a);
^~~~~~~~~~~~~~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/test_comptime_invalid_enum_cast.zig:1:13: note: enum declared here
const Foo = enum(u2) {
^~~~
값이 컴파일 타임에 확정되지 않고 런타임에 들어온다면, 컴파일러가 미리 잡아내지 못해요. 이 경우엔 프로그램이 안전 검사에 걸려 패닉합니다.
const std = @import("std");
const Foo = enum(u2) {
a,
b,
c,
};
fn foo(a: u2) void {
const b: Foo = @fromBackingInt(a);
std.debug.print("value: {s}\n", .{@tagName(b)});
}
pub fn main() void {
foo(3);
}
$ zig build-exe runtime_invalid_enum_cast.zig
$ ./runtime_invalid_enum_cast
thread 972816 panic: invalid enum value
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_invalid_enum_cast.zig:10:20: 0x11e839b in foo (runtime_invalid_enum_cast.zig)
const b: Foo = @fromBackingInt(a);
^
/home/ci/work/zig-bootstrap/zig/doc/langref/runtime_invalid_enum_cast.zig:15:8: 0x11e828e in main (runtime_invalid_enum_cast.zig)
foo(3);
^
... (process terminated by signal)
panic: invalid enum value라는 메시지와 함께 비정상 종료돼요. 이렇게 대응 값이 없는 enum 변환은 컴파일 타임엔 컴파일 오류로, 런타임엔 안전 검사 패닉으로 이어지는 거예요. enum으로 변환하기 전에 정수 값이 유효한 태그 범위 안에 있는지 확인하는 걸 잊지 말아야 해요.
더 알아보기 (Learn more)
이 주제를 더 깊이 이해하려면 아래를 함께 보면 좋아요.
- @intFromEnum — enum 값을 정수 태그 타입으로 바꾸는 내장 함수
- @enumFromInt — 정수를 enum 값으로 바꾸는 내장 함수 (deprecated)
- @backingInt — enum의 backing 정수 값을 직접 다루는 권장 함수
- @fromBackingInt — backing 정수 값을 enum 값으로 되돌리는 권장 함수
- IntCast — 안전하지 않을 수 있는 정수 형 변환
- enum — enum 자체에 대한 전반적인 내용