在開發程式的時候,我們很常需要產生出有獨立名稱,且為連續或是有規律的數值,用來當作介面的參數數值。舉例來說,有一個函數擁有10種工作模式,可以透過該函數的第一個參數來傳入數值0~9來決定要使用哪一種模式,為了方便輸入,且為了確保輸入的數值是在0~9,我們通常會使用一種特殊結構作為這個參數的型別,來代入數值0~9。如果程式語言支援列舉(enum),這個功能通常會直接使用列舉來實現。但是Rust程式語言的列舉有個小缺點,那就是它不方便直接利用列舉變體的「序數」(ordinal),也就是那個變體在該列舉中被定義出來的順序,作為程式邏輯的應用。另外,如果要透過「序數」來取得對應的列舉變體,並沒有內建安全的作法。



內建取得列舉變體「序數」的作法

如果要得到某列舉變體的序數,必須要使用as關鍵字,直接將列舉實體轉型。程式如下:

enum Mode {
    Mode1,
    Mode2,
    Mode3,
}

fn main() {
    assert_eq!(2, Mode::Mode3 as u8);
}

但如果是列舉實體的參考,則無法使用as關鍵字來進行轉型。舉例來說:

enum Mode {
    Mode1,
    Mode2,
    Mode3,
}

struct Config {
    mode: Mode
}

fn foo(m: &Mode) {
    assert_eq!(2, m as u8);
}

fn main() {
    let config = Config {
        mode: Mode::Mode3
    };
    
    foo(&config.mode);
}

以上程式,第12行會編譯失敗。需將程式修改成:

#[derive(Clone)]
enum Mode {
    Mode1,
    Mode2,
    Mode3,
}

struct Config {
    mode: Mode
}

fn foo(m: Mode) {
    assert_eq!(2, m as u8);
}

fn main() {
    let config = Config {
        mode: Mode::Mode3
    };

    foo(config.mode.clone());
}

所以說這個方式並不太好用。

Enum Ordinalize

「Enum Ordinalize」是筆者開發的程序式巨集套件,可以讓列舉實作Oridinal特性,使其擁有from_ordinal_unsafefrom_ordinal關聯函數,以及ordinal方法,還有VARIANT_COUNTVARIANTSVALUES常數。

Crates.io

Cargo.toml

enum-ordinalize = "*"

使用方法

使用#[derive(Ordinalize)]屬性可以讓只有單元變體(Unit Variant)的列舉實作Ordinalize特性。

use enum_ordinalize::Ordinalize;

#[derive(Debug, PartialEq, Eq, Ordinalize)]
enum MyEnum {
    Zero,
    One,
    Two,
}

assert_eq!(3, MyEnum::VARIANT_COUNT);
assert_eq!([MyEnum::Zero, MyEnum::One, MyEnum::Two], MyEnum::VARIANTS);
assert_eq!([0i8, 1i8, 2i8], MyEnum::VALUES);

assert_eq!(0i8, MyEnum::Zero.ordinal());
assert_eq!(1i8, MyEnum::One.ordinal());
assert_eq!(2i8, MyEnum::Two.ordinal());

assert_eq!(Some(MyEnum::Zero), MyEnum::from_ordinal(0i8));
assert_eq!(Some(MyEnum::One), MyEnum::from_ordinal(1i8));
assert_eq!(Some(MyEnum::Two), MyEnum::from_ordinal(2i8));

assert_eq!(MyEnum::Zero, unsafe { MyEnum::from_ordinal_unsafe(0i8) });
assert_eq!(MyEnum::One, unsafe { MyEnum::from_ordinal_unsafe(1i8) });
assert_eq!(MyEnum::Two, unsafe { MyEnum::from_ordinal_unsafe(2i8) });

「序數」的大小

序數的大小是由列舉自身的大小來決定的。列舉的變體所表示的值愈大(若為負數則是愈小),則列舉的大小愈大。

例如:

use enum_ordinalize::Ordinalize;

#[derive(Debug, PartialEq, Eq, Ordinalize)]
enum MyEnum {
    Zero,
    One,
    Two,
    Thousand = 1000,
}

assert_eq!(4, MyEnum::VARIANT_COUNT);
assert_eq!([MyEnum::Zero, MyEnum::One, MyEnum::Two, MyEnum::Thousand], MyEnum::VARIANTS);
assert_eq!([0i16, 1i16, 2i16, 1000i16], MyEnum::VALUES);

assert_eq!(0i16, MyEnum::Zero.ordinal());
assert_eq!(1i16, MyEnum::One.ordinal());
assert_eq!(2i16, MyEnum::Two.ordinal());

assert_eq!(Some(MyEnum::Zero), MyEnum::from_ordinal(0i16));
assert_eq!(Some(MyEnum::One), MyEnum::from_ordinal(1i16));
assert_eq!(Some(MyEnum::Two), MyEnum::from_ordinal(2i16));

assert_eq!(MyEnum::Zero, unsafe { MyEnum::from_ordinal_unsafe(0i16) });
assert_eq!(MyEnum::One, unsafe { MyEnum::from_ordinal_unsafe(1i16) });
assert_eq!(MyEnum::Two, unsafe { MyEnum::from_ordinal_unsafe(2i16) });

為了要儲存1000MyEnum列舉的大小會增長,序數的大小會變成i16,而不是i8

您也可以使用#[repr(型別)]屬性來明確控制大小。舉例來說:

use enum_ordinalize::Ordinalize;

#[derive(Debug, PartialEq, Eq, Ordinalize)]
#[repr(usize)]
enum MyEnum {
    Zero,
    One,
    Two,
    Thousand = 1000,
}

assert_eq!(4, MyEnum::VARIANT_COUNT);
assert_eq!([MyEnum::Zero, MyEnum::One, MyEnum::Two, MyEnum::Thousand], MyEnum::VARIANTS);
assert_eq!([0usize, 1usize, 2usize, 1000usize], MyEnum::VALUES);

assert_eq!(0usize, MyEnum::Zero.ordinal());
assert_eq!(1usize, MyEnum::One.ordinal());
assert_eq!(2usize, MyEnum::Two.ordinal());

assert_eq!(Some(MyEnum::Zero), MyEnum::from_ordinal(0usize));
assert_eq!(Some(MyEnum::One), MyEnum::from_ordinal(1usize));
assert_eq!(Some(MyEnum::Two), MyEnum::from_ordinal(2usize));

assert_eq!(MyEnum::Zero, unsafe { MyEnum::from_ordinal_unsafe(0usize) });
assert_eq!(MyEnum::One, unsafe { MyEnum::from_ordinal_unsafe(1usize) });
assert_eq!(MyEnum::Two, unsafe { MyEnum::from_ordinal_unsafe(2usize) });

好用的自動累進

變體所代表的整數是連續自動累進的,我們也可以在累進的同時,於任意的變體上設定明確的數值。

use enum_ordinalize::Ordinalize;

#[derive(Debug, PartialEq, Eq, Ordinalize)]
enum MyEnum {
    Two   = 2,
    Three,
    Four,
    Eight = 8,
    Nine,
    NegativeTen = -10,
    NegativeNine,
}

assert_eq!(7, MyEnum::VARIANT_COUNT);
assert_eq!([MyEnum::Two, MyEnum::Three, MyEnum::Four, MyEnum::Eight, MyEnum::Nine, MyEnum::NegativeTen, MyEnum::NegativeNine], MyEnum::VARIANTS);
assert_eq!([2i8, 3i8, 4i8, 8i8, 9i8, -10i8, -9i8], MyEnum::VALUES);

assert_eq!(4i8, MyEnum::Four.ordinal());
assert_eq!(9i8, MyEnum::Nine.ordinal());
assert_eq!(-9i8, MyEnum::NegativeNine.ordinal());

assert_eq!(Some(MyEnum::Four), MyEnum::from_ordinal(4i8));
assert_eq!(Some(MyEnum::Nine), MyEnum::from_ordinal(9i8));
assert_eq!(Some(MyEnum::NegativeNine), MyEnum::from_ordinal(-9i8));

assert_eq!(MyEnum::Four, unsafe { MyEnum::from_ordinal_unsafe(4i8) });
assert_eq!(MyEnum::Nine, unsafe { MyEnum::from_ordinal_unsafe(9i8) });
assert_eq!(MyEnum::NegativeNine, unsafe { MyEnum::from_ordinal_unsafe(-9i8) });

在列舉本身實作功能

基於某些情形,如果我們不想要替我們的列舉實作Ordinalize特性,也可以選擇禁用特性的實作,逐一設定有用到的常數和函數。函數會是常數函數。名稱和存取權限也都可以由我們來定義。

use enum_ordinalize::Ordinalize;

#[derive(Debug, PartialEq, Eq, Ordinalize)]
#[ordinalize(impl_trait = false)]
#[ordinalize(variant_count(pub const VARIANT_COUNT, doc = "The count of variants."))]
#[ordinalize(variants(pub const VARIANTS, doc = "List of this enum's variants."))]
#[ordinalize(values(pub const VALUES, doc = "List of values for all variants of this enum."))]
#[ordinalize(ordinal(pub const fn ordinal, doc = "Retrieve the integer number of this variant."))]
#[ordinalize(from_ordinal(pub const fn from_ordinal, doc = "Obtain a variant based on an integer number."))]
#[ordinalize(from_ordinal_unsafe(
    pub const fn from_ordinal_unsafe,
    doc = "Obtain a variant based on an integer number.",
    doc = "# Safety",
    doc = "You have to ensure that the input integer number can correspond to a variant on your own.",
))]
enum MyEnum {
    A,
    B,
}

assert_eq!(2, MyEnum::VARIANT_COUNT);
assert_eq!([MyEnum::A, MyEnum::B], MyEnum::VARIANTS);
assert_eq!([0i8, 1i8], MyEnum::VALUES);

assert_eq!(0i8, MyEnum::A.ordinal());
assert_eq!(1i8, MyEnum::B.ordinal());

assert_eq!(Some(MyEnum::A), MyEnum::from_ordinal(0i8));
assert_eq!(Some(MyEnum::B), MyEnum::from_ordinal(1i8));

assert_eq!(MyEnum::A, unsafe { MyEnum::from_ordinal_unsafe(0i8) });
assert_eq!(MyEnum::B, unsafe { MyEnum::from_ordinal_unsafe(1i8) });