enum(열거형)

enum(열거형)

관련 있는 값들을 이름으로 묶어서 다루고 싶을 때가 있어요. 그럴 때 Zig의 enum(열거형)을 쓰면 됩니다. 이번에는 enum을 선언하는 방법부터, 값에 서수(ordinal)를 붙이는 법, 메서드를 정의하는 법, 그리고 외부 C와 호환되는 extern enum, 타입을 생략하는 Enum Literal, 확장 가능한 Non-exhaustive enum까지 하나씩 살펴볼게요.

출처: Zig Documentation

본문

가장 기본적인 enum부터 볼게요. enum 키워드로 값을 나열해서 선언하고, .이름 점 문법으로 특정 필드를 골라 쓸 수 있어요. enum의 서수값을 직접 다루고 싶다면 태그 타입(tag type)을 정수로 지정하면 됩니다.

test_enums.zig

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,
};

// Declare a specific enum field.
const c = Type.ok;

// 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));
}

// 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));
}

// 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));
}

// 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());
}

// 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 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 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 gives a [:0]const u8 representation of an enum value:
test "@tagName" {
    try expectEqualStrings(@tagName(Small.three), "three");
}

// Empty enums are uninstantiable, their tag type is always noreturn.
const Empty = enum {};
test "empty enum" {
    try expectEqual(noreturn, @typeInfo(Empty).@"enum".tag_type);
}
$ 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.

코드를 천천히 따라가 볼게요. enum(u2)처럼 태그 타입을 지정하면 서수값이 0부터 시작해 이전 멤버에서 1씩 증가해요. 이 서수값은 @backingInt로 꺼낼 수 있고, 반대로 명시적으로 hundred = 100처럼 값을 지정해서 서수값을 바꿀 수도 있어요. 값을 일부만 지정하면(Value3) 지정하지 않은 a, c, e는 자동으로 채워집니다. 이 규칙이 바로 '이전 멤버 + 1'이라서, b = 8 다음인 c는 9가 되는 거예요.

enum에는 struct나 union처럼 메서드를 정의할 수 있어요. 특별한 게 아니라, 점 문법으로 호출할 수 있는 네임스페이스된 함수일 뿐입니다. 또 enum은 switch 문의 대상이 될 수 있고, @typeInfo로 태그 타입이나 필드 이름·개수를, @tagName으로 값의 이름을 문자열로 얻을 수 있어요. 마지막으로 필드가 하나도 없는 빈 enum은 인스턴스를 만들 수 없고, 태그 타입이 항상 noreturn이 됩니다.

참고로 enum과 관련된 내장 함수들도 함께 봐 두면 좋아요.

  • @backingInt — enum 값의 서수(정수)를 꺼냅니다
  • @fromBackingInt — 정수에서 enum 값으로 바꿉니다
  • @typeInfo — 태그 타입·필드 정보를 조회합니다
  • @tagName — 값의 이름을 [:0]const u8로 얻습니다
  • @sizeOf — 타입의 크기를 구합니다
  • noreturn — 빈 enum이 쓰는 특수 타입

extern enum

기본적으로 enum은 C ABI와의 호환이 보장되지 않아요. 실제로 그걸 확인하는 코드부터 볼게요.

enum_export_error.zig

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

태그 타입을 지정하지 않은 enum을 C 호출 규약을 쓰는 export fn의 인자로 넘기면, 오류 메시지가 말해 주듯 '정수 태그 타입이 추론되어 있지 않다'는 이유로 컴파일이 거부돼요. C ABI와 호환되게 하려면 enum에 명시적인 태그 타입을 주면 됩니다.

enum_export.zig

const Foo = enum(c_int) { a, b, c };
export fn entry(foo: Foo) void {
    _ = foo;
}
$ zig build-obj enum_export.zig

enum(c_int)처럼 태그 타입을 정수로 지정해 주면, 이제 C와 주고받을 수 있는 enum이 됩니다. 태그 타입이 명시되니 컴파일도 그대로 성공하는 걸 확인할 수 있어요.

Enum Literals

enum 리터럴(Enum Literal)은 enum 타입을 명시하지 않고도 enum 필드의 이름만으로 값을 지정하는 방법이에요.

test_enum_literals.zig

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.

.auto처럼 타입을 빼고 이름만 써도 돼요. 타입이 Color로 정해진 변수 자리에 오면 컴파일러가 그 타입을 보고 .autoColor.auto임을 알아냅니다. switch에서 분기할 때도 .on 같은 리터럴 형태로 깔끔하게 쓸 수 있어요. 이렇게 Color.auto.auto는 같은 값을 나타냅니다.

Non-exhaustive enum

Non-exhaustive enum(비-완전 열거형)은 끝에 _ 필드를 하나 추가해서 만든 enum이에요. 이 enum은 반드시 태그 타입을 지정해야 하고, 모든 열거 값을 다 차지해서는 안 됩니다.

@fromBackingInt를 non-exhaustive enum에 적용하면 항상 유효한 enum 값이 나와요. 그리고 non-exhaustive enum을 switch로 분기할 때는, else 대신 _ prong을 쓸 수 있어요. _ prong을 쓰면 알려진 모든 태그 이름을 switch가 처리하지 않을 경우에 컴파일러가 오류를 냅니다.

test_switch_non-exhaustive.zig

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.

Number enum에 _ 멤버가 들어 있죠. 이 덕분에 switch에서 _ prong으로 나머지 경우를 처리할 수 있고, 알려진 이름(one, two, three)을 빠뜨리면 컴파일러가 잡아 줍니다. 물론 else prong으로도 처리할 수 있어요. non-exhaustive enum은 나중에 값이 더 추가될 수 있는, 그래서 컴파일 타임에 모든 경우를 확정할 수 없는 상황에 유용해요.

더 알아보기