열거형

열거형 (Enumerations)

여러 가능한 상태 중 하나를 나타내고 싶을 때, Rust에서 가장 자연스러운 도구가 열거형(enum)이에요. enum은 명목상의 열거형 타입과 그 타입의 값을 만들거나 패턴 매칭하는 데 쓰는 생성자(constructor) 집합을 동시에 정의합니다.

출처: Rust Reference

본문

열거형(enum)은 명목상의 열거형 타입과, 그 타입의 값을 만들거나 패턴 매칭하는 데 사용되는 생성자 세트를 동시에 정의해요.

문법

Enumeration →
    enum IDENTIFIER GenericParams? WhereClause? { EnumVariants? }

EnumVariants → EnumVariant ( , EnumVariant )* ,?

EnumVariant →
    OuterAttribute* Visibility?
    IDENTIFIER ( EnumVariantTuple | EnumVariantStruct )? EnumVariantDiscriminant?

EnumVariantTuple → ( TupleFields? )

EnumVariantStruct → { StructFields? }

EnumVariantDiscriminant → = Expression

열거형은 키워드 enum으로 선언해요.

enum 선언은 그 위치한 모듈이나 블록의 타입 네임스페이스에 열거형 타입을 정의합니다.

enum 아이템과 그 사용 예시를 보면:

#![allow(unused)]
fn main() {
enum Animal {
    Dog,
    Cat,
}

let mut a: Animal = Animal::Dog;
a = Animal::Cat;
}

생성자의 필드

enum 생성자는 이름 있는 필드 또는 이름 없는 필드를 가질 수 있어요.

#![allow(unused)]
fn main() {
enum Animal {
    Dog(String, f64),
    Cat { name: String, weight: f64 },
}

let mut a: Animal = Animal::Dog("Cocoa".to_string(), 37.2);
a = Animal::Cat { name: "Spotty".to_string(), weight: 2.7 };
}

이 예시에서 Cat구조체형(struct-like) enum 변형이고, Dog는 그냥 'enum 변형'이라고 불러요.

필드 없는 enum (Field-less enums)

생성자 중 어느 것도 필드를 담고 있지 않은 enum을 필드 없는(field-less) enum이라 해요. 예를 들면:

#![allow(unused)]
fn main() {
enum Fieldless {
    Tuple(),
    Struct{},
    Unit,
}
}

유닛 전용(unit-only) enum

필드 없는 enum이 유닛 변형만 포함하면 유닛 전용(unit-only) enum이라고 불러요.

#![allow(unused)]
fn main() {
enum Enum {
    Foo = 3,
    Bar = 2,
    Baz = 1,
}
}

변형 생성자

변형(variant) 생성자는 구조체 정의와 비슷하며, use 선언을 포함해 열거형 이름에서 오는 경로로 참조할 수 있어요.

각 변형은 타입 네임스페이스에 자신의 타입을 정의해요. 다만 그 타입을 타입 지정자로 사용할 수는 없어요. 튜플형과 유닛형 변형은 또한 값 네임스페이스에 생성자를 정의합니다.

예를 들어:

#![allow(unused)]
fn main() {
enum Examples {
    UnitLike,
    TupleLike(i32),
    StructLike { value: i32 },
}

use Examples::*; // 모든 변형의 별칭을 만든다.
let x = UnitLike; // 상수 아이템의 경로 표현식
let x = UnitLike {}; // 구조체 표현식
let y = TupleLike(123); // 호출 표현식
let y = TupleLike { 0: 123 }; // 정수 필드 이름을 쓰는 구조체 표현식
let z = StructLike { value: 123 }; // 구조체 표현식
}

판별자 (Discriminants)

각 enum 인스턴스는 **판별자(discriminant)**를 가져요. 이는 그 인스턴스에 논리적으로 연관된 정수로, 어떤 변형을 담고 있는지 결정하는 데 사용됩니다.

Rust 표현(repr)에서 판별자는 isize 값으로 해석됩니다. 다만 컴파일러는 실제 메모리 레이아웃에서 더 작은 타입(또는 변형을 구분하는 다른 수단)을 사용할 수 있어요.

판별자 값 할당 (Assigning discriminant values)

명시적 판별자 (Explicit discriminants)

두 가지 상황에서 변형의 판별자를 명시적으로 설정할 수 있어요. 변형 이름 뒤에 =상수 표현식을 붙이면 됩니다.

예를 들어:

#![allow(unused)]
fn main() {
#[repr(u8)]
enum Enum {
    Unit = 3,
    Tuple(u16),
    Struct {
        a: u8,
        b: u16,
    } = 1,
}
}

암시적 판별자 (Implicit discriminants)

변형의 판별자가 지정되지 않았다면, 선언에서 이전 변형의 판별자보다 1 큰 값으로 설정돼요. 선언에서 첫 번째 변형의 판별자가 지정되지 않으면 0으로 설정됩니다.

#![allow(unused)]
fn main() {
enum Foo {
    Bar,            // 0
    Baz = 123,      // 123
    Quux,           // 124
}

let baz_discriminant = Foo::Baz as u32;
assert_eq!(baz_discriminant, 123);
}

제한사항 (Restrictions)

두 변형이 같은 판별자를 공유하는 것은 오류예요.

#![allow(unused)]
fn main() {
enum SharedDiscriminantError {
    SharedA = 1,
    SharedB = 1,
}

enum SharedDiscriminantError2 {
    Zero,       // 0
    One,        // 1
    OneToo = 1, // 1 (이전 것과 충돌!)
}
}

또한 이전 판별자가 판별자 크기의 최댓값인데 판별자가 지정되지 않은 경우도 오류예요.

#![allow(unused)]
fn main() {
#[repr(u8)]
enum OverflowingDiscriminantError {
    Max = 255,
    MaxPlusOne, // 256이 되지만 enum을 오버플로한다.
}

#[repr(u8)]
enum OverflowingDiscriminantError2 {
    MaxMinusOne = 254, // 254
    Max,               // 255
    MaxPlusOne,        // 256이 되지만 enum을 오버플로한다.
}
}

명시적 enum 판별자 초기화자는 바깥 enum의 제네릭 매개변수를 사용할 수 없어요.

#![allow(unused)]
fn main() {
#[repr(u32)]
enum E<'a, T, const N: u32> {
    Lifetime(&'a T) = {
        let a: &'a (); // ERROR.
        1
    },
    Type(T) = {
        let x: T; // ERROR.
        2
    },
    Const = N, // ERROR.
}
}

판별자 접근 (Accessing discriminant)

mem::discriminant로 접근하기

std::mem::discriminant는 enum 값의 판별자에 대한 불투명한 참조를 반환하며, 이를 비교할 수 있어요. 하지만 이걸로 판별자 값 자체를 얻을 수는 없어요.

캐스팅 (Casting)

enum이 유닛 전용(튜플·구조체 변형 없음)이라면, 판별자에 숫자 캐스트(numeric cast)로 직접 접근할 수 있어요.

#![allow(unused)]
fn main() {
enum Enum {
    Foo,
    Bar,
    Baz,
}

assert_eq!(0, Enum::Foo as isize);
assert_eq!(1, Enum::Bar as isize);
assert_eq!(2, Enum::Baz as isize);
}

필드 없는 enum명시적 판별자가 없거나, 유닛 변형만 명시적일 때 캐스트할 수 있어요.

#![allow(unused)]
fn main() {
enum Fieldless {
    Tuple(),
    Struct{},
    Unit,
}

assert_eq!(0, Fieldless::Tuple() as isize);
assert_eq!(1, Fieldless::Struct{} as isize);
assert_eq!(2, Fieldless::Unit as isize);

#[repr(u8)]
enum FieldlessWithDiscriminants {
    First = 10,
    Tuple(),
    Second = 20,
    Struct{},
    Unit,
}

assert_eq!(10, FieldlessWithDiscriminants::First as u8);
assert_eq!(11, FieldlessWithDiscriminants::Tuple() as u8);
assert_eq!(20, FieldlessWithDiscriminants::Second as u8);
assert_eq!(21, FieldlessWithDiscriminants::Struct{} as u8);
assert_eq!(22, FieldlessWithDiscriminants::Unit as u8);
}

포인터 캐스팅 (Pointer casting)

enum이 기본 표현(primitive representation)을 지정한다면, unsafe 포인터 캐스팅으로 판별자를 안정적으로 접근할 수 있어요.

#![allow(unused)]
fn main() {
#[repr(u8)]
enum Enum {
    Unit,
    Tuple(bool),
    Struct{a: bool},
}

impl Enum {
    fn discriminant(&self) -> u8 {
        unsafe { *(self as *const Self as *const u8) }
    }
}

let unit_like = Enum::Unit;
let tuple_like = Enum::Tuple(true);
let struct_like = Enum::Struct{a: false};

assert_eq!(0, unit_like.discriminant());
assert_eq!(1, tuple_like.discriminant());
assert_eq!(2, struct_like.discriminant());
}

변형이 없는 enum (Zero-variant enums)

변형이 없는 enum을 제로 변형(zero-variant) enum이라고 해요. 유효한 값이 없기 때문에 인스턴스화할 수 없습니다.

#![allow(unused)]
fn main() {
enum ZeroVariants {}
}

제로 변형 enum은 never 타입과 동등하지만, 다른 타입으로 강제 변환될 수는 없어요.

#![allow(unused)]
fn main() {
enum ZeroVariants {}
let x: ZeroVariants = panic!();
let y: u32 = x; // 타입이 안 맞는다는 오류
}

변형 가시성 (Variant visibility)

enum 변형은 문법적으로 가시성(Visibility) 주석을 허용하지만, enum이 검증될 때 이는 거부돼요. 이는 아이템이 사용되는 다양한 맥락에서 통일된 문법으로 파싱되도록 하기 위함이에요.

#![allow(unused)]
fn main() {
macro_rules! mac_variant {
    ($vis:vis $name:ident) => {
        enum $name {
            $vis Unit,

            $vis Tuple(u8, u16),

            $vis Struct { f: u8 },
        }
    }
}

// 빈 `vis`도 허용된다.
mac_variant! { E }

// 검증 전에 제거되므로 허용된다.
#[cfg(false)]
enum E {
    pub U,
    pub(crate) T(u8),
    pub(super) T { f: String },
}
}

더 알아보기