enum
enum (열거형)
몇 개의 정해진 값 중 하나만 갖는 타입이 필요할 때가 있어요. 그럴 때 바로 enum을 쓰면 돼요. enum은 서로 관련된 이름들을 하나의 타입으로 묶어 주고, 각 이름에는 자동으로 순서 값(ordinal value)이 붙어요. struct나 union과 똑같이 메서드도 붙일 수 있고, switch와도 찰떡궁합이죠. 어떤 값이 "이 목록 중 하나"임을 보장받고 싶을 때 enum이 딱입니다.
본문
enum 선언과 기본 사용법
enum은 이렇게 선언해요. 먼저 std의 테스트 헬퍼들을 불러온 뒤, Type이라는 enum을 정의해 봅시다.
const expect = @import("std").testing.expect;
const expectEqual = @import("std").testing.expectEqual;
const expectEqualStrings = @import("std").testing.expectEqualStrings;
const mem = @import("std").mem;
// Declare an enum.
const Type = enum {
ok,
not_ok,
};
선언한 enum의 특정 필드는 점 표기법으로 이렇게 가리킬 수 있어요.
// Declare a specific enum field.
const c = Type.ok;
enum의 순서 값(ordinal value, 0부터 시작하는 정수)에 접근하고 싶다면 태그 타입(tag type)을 명시해 주면 돼요. 아래처럼 enum(u2)라고 쓰면 u2와 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));
}
순서 값을 직접 덮어쓸 수도 있어요. @backingInt로 실제 정수 값을 꺼내 보면 내가 지정한 값 그대로 나옵니다.
// You can override the ordinal value for an enum.
const Value2 = enum(u32) {
hundred = 100,
thousand = 1000,
million = 1000000,
};
test "set enum ordinal value" {
try expectEqual(100, @backingInt(Value2.hundred));
try expectEqual(1000, @backingInt(Value2.thousand));
try expectEqual(1000000, @backingInt(Value2.million));
}
딱 일부 값만 덮어쓰기도 가능해요. 이렇게 하면 안 덮어쓴 항목은 이전 항목의 값에 1을 더한 값이 자동으로 부여돼요. 아래 예시를 보면 b = 8, c = 9(b에 1을 더한 값), d = 4, e = 5(d에 1을 더한 값)가 됩니다.
// You can also override only some values.
const Value3 = enum(u4) {
a,
b = 8,
c,
d = 4,
e,
};
test "enum implicit ordinal values and overridden values" {
try expectEqual(0, @backingInt(Value3.a));
try expectEqual(8, @backingInt(Value3.b));
try expectEqual(9, @backingInt(Value3.c));
try expectEqual(4, @backingInt(Value3.d));
try expectEqual(5, @backingInt(Value3.e));
}
enum 메서드
enum에도 struct나 union처럼 메서드를 붙일 수 있어요. enum 메서드가 특별히 대우받는 건 아니고, 그냥 점 표기법으로 호출할 수 있게 이름 공간(namespaced)만 잡아 준 함수일 뿐입니다.
// Enums can have methods, the same as structs and unions.
// Enum methods are not special, they are only namespaced
// functions that you can call with dot syntax.
const Suit = enum {
clubs,
spades,
diamonds,
hearts,
pub fn isClubs(self: Suit) bool {
return self == Suit.clubs;
}
};
test "enum method" {
const p = Suit.spades;
try expect(!p.isClubs());
}
enum과 switch
enum은 switch에 아주 잘 어울려요. enum의 가능한 값이 정해져 있으니, 컴파일러가 모든 경우를 다 처리했는지 꼼꼼하게 검사해 줍니다.
// An enum can be switched upon.
const Foo = enum {
string,
number,
none,
};
test "enum switch" {
const p = Foo.number;
const what_is_it = switch (p) {
Foo.string => "this is a string",
Foo.number => "this is a number",
Foo.none => "this is a none",
};
try expectEqualStrings(what_is_it, "this is a number");
}
@typeInfo로 enum 살펴보기
@typeInfo를 쓰면 enum의 정수 태그 타입에 접근할 수 있어요. 아래 예시처럼 Small에는 항목이 네 개이니 태그 타입은 u2가 됩니다.
// @typeInfo can be used to access the integer tag type of an enum.
const Small = enum {
one,
two,
three,
four,
};
test "std.meta.Tag" {
try expectEqual(u2, @typeInfo(Small).@"enum".tag_type);
}
@typeInfo는 필드의 개수와 필드 이름들도 알려줘요. 필드 이름은 배열로 들어오니 .len으로 개수를, 인덱스로 특정 이름을 가져올 수 있습니다.
// @typeInfo tells us the field count and the fields names:
test "@typeInfo" {
try expectEqual(4, @typeInfo(Small).@"enum".field_names.len);
try expectEqualStrings(@typeInfo(Small).@"enum".field_names[1], "two");
}
@tagName으로 이름 얻기
@tagName은 enum 값을 [:0]const u8 형태의 문자열로 바꿔 줘요. 값의 "이름"이 필요할 때 유용하죠.
// @tagName gives a [:0]const u8 representation of an enum value:
test "@tagName" {
try expectEqualStrings(@tagName(Small.three), "three");
}
빈 enum
빈 enum은 인스턴스를 만들 수 없어요. 태그 타입이 항상 noreturn이 되죠.
// Empty enums are uninstantiable, their tag type is always noreturn.
const Empty = enum {};
test "empty enum" {
try expectEqual(noreturn, @typeInfo(Empty).@"enum".tag_type);
}
위 예제(test_enums.zig)를 돌려 보면 모든 테스트가 통과하는 걸 확인할 수 있어요.
$ zig test test_enums.zig
1/9 test_enums.test.enum ordinal value...OK
2/9 test_enums.test.set enum ordinal value...OK
3/9 test_enums.test.enum implicit ordinal values and overridden values...OK
4/9 test_enums.test.enum method...OK
5/9 test_enums.test.enum switch...OK
6/9 test_enums.test.std.meta.Tag...OK
7/9 [email protected]
8/9 [email protected]
9/9 test_enums.test.empty enum...OK
All 9 tests passed.
extern enum (C ABI 호환 enum)
기본적으로 enum은 C ABI와 호환된다는 보장이 없어요. 그래서 그냥 만든 enum을 export 함수의 매개변수로 쓰려고 하면 이런 오류가 납니다. "enum의 정수 태그 타입이 추론되었으니, 명시적으로 지정해 보라"는 뜻이에요.
const Foo = enum { a, b, c };
export fn entry(foo: Foo) void {
_ = foo;
}
$ zig build-obj enum_export_error.zig -target x86_64-linux
/home/ci/work/zig-bootstrap/zig/doc/langref/enum_export_error.zig:2:17: error: parameter of type 'enum_export_error.Foo' not allowed in function with calling convention 'x86_64_sysv'
export fn entry(foo: Foo) void {
^~~~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/enum_export_error.zig:1:13: note: integer tag type of enum is inferred
const Foo = enum { a, b, c };
^~~~~~~~~~~~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/enum_export_error.zig:1:13: note: consider explicitly specifying the integer tag type
/home/ci/work/zig-bootstrap/zig/doc/langref/enum_export_error.zig:1:13: note: enum declared here
referenced by:
root: /home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:13:22
comptime: /home/ci/work/zig-bootstrap/out/host/lib/zig/std/start.zig:20:9
2 reference(s) hidden; use '-freference-trace=4' to see all references
C ABI와 호환되는 enum이 필요하다면, enum에 명시적인 태그 타입을 지정해 주면 돼요. 이번엔 오류 없이 잘 컴파일됩니다.
const Foo = enum(c_int) { a, b, c };
export fn entry(foo: Foo) void {
_ = foo;
}
$ zig build-obj enum_export.zig
Enum Literals (enum 리터럴)
enum 리터럴을 쓰면 타입을 밝히지 않고도 enum 필드의 이름만으로 값을 지정할 수 있어요. 점(.)으로 시작하는 .auto 같은 형태가 바로 enum 리터럴이에요. 타입이 문맥에서 추론될 수 있을 때 특히 유용합니다.
const std = @import("std");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const Color = enum {
auto,
off,
on,
};
test "enum literals" {
const color1: Color = .auto;
const color2 = Color.auto;
try expectEqual(color1, color2);
}
test "switch using enum literals" {
const color = Color.on;
const result = switch (color) {
.auto => false,
.on => true,
.off => false,
};
try expect(result);
}
$ zig test test_enum_literals.zig
1/2 test_enum_literals.test.enum literals...OK
2/2 test_enum_literals.test.switch using enum literals...OK
All 2 tests passed.
Non-exhaustive enum (비-완전 enum)
끝에 _ 필드를 추가하면 non-exhaustive enum을 만들 수 있어요. 이 enum은 태그 타입을 반드시 지정해야 하고, 모든 열거 값을 다 소비할 수는 없어요.
@fromBackingInt를 non-exhaustive enum에 적용하면 항상 유효한 enum 값이 나온답니다.
non-exhaustive enum에 대한 switch에서는 else 프롱 대신 _ 프롱을 쓸 수 있어요. _ 프롱을 쓰면, 알고 있는 모든 태그 이름을 switch가 처리하지 않으면 컴파일러가 오류를 냅니다.
const std = @import("std");
const expect = std.testing.expect;
const Number = enum(u8) {
one,
two,
three,
_,
};
test "switch on non-exhaustive enum" {
const number = Number.one;
const result = switch (number) {
.one => true,
.two, .three => false,
_ => false,
};
try expect(result);
const is_one = switch (number) {
.one => true,
else => false,
};
try expect(is_one);
}
$ zig test test_switch_non-exhaustive.zig
1/1 test_switch_non-exhaustive.test.switch on non-exhaustive enum...OK
All 1 tests passed.
더 알아보기 (Learn more)
enum을 더 깊이 이해하려면 아래 주제를 함께 보면 좋아요.
- @backingInt — enum 순서 값(ordinal value)을 정수로 꺼내는 내장 함수
- @fromBackingInt — 정수를 enum 값으로 되돌리는 내장 함수
- @typeInfo — 타입의 메타데이터에 접근하는 내장 함수
- @tagName — enum 값을 이름 문자열로 바꾸는 내장 함수
- @sizeOf — 타입의 크기(바이트)를 구하는 내장 함수
- noreturn — 값이 절대 존재하지 않는 타입