열거형
열거형 (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)
두 가지 상황에서 변형의 판별자를 명시적으로 설정할 수 있어요. 변형 이름 뒤에 =와 상수 표현식을 붙이면 됩니다.
- enum이 "유닛 전용"일 때
- 기본 표현(primitive representation)을 사용할 때
예를 들어:
#![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 },
}
}