在開發程式的時候,我們很常需要產生出有獨立名稱,且為連續或是有規律的數值,用來當作介面的參數數值。舉例來說,有一個函數擁有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」是筆者開發的套件,可以讓新建立出來的列舉擁有「ordinal」、「from_ordinal」和「from_ordinal_unsafe」方法,列舉的序數的型別均為isize。

Crates.io

https://crates.io/crates/enum-ordinalize

Cargo.toml

enum-ordinalize = "*"

巨集的使用

Enum Ordinalize提供的「create_ordinalized_enum」巨集可以快速產生出擁有「ordinal」、「from_ordinal」和「from_ordinal_unsafe」方法且實作了「Debug」、「PartialEq」、「Clone」、「Eq」和「Hash」特性的列舉。

例如:

#[macro_use]
extern crate enum_ordinalize;

create_ordinalized_enum!(MyEnum,
    Zero,
    One,
    Two
);

assert_eq!(2, MyEnum::Two.ordinal());
assert_eq!(Some(MyEnum::One), MyEnum::from_ordinal(1));
assert_eq!(MyEnum::One, unsafe{MyEnum::from_ordinal_unsafe(1)});

「from_ordinal」和「from_ordinal_unsafe」方法的差異在於,「from_ordinal_unsafe」方法的轉換方式是直接使用相同的記憶體空間的值,效能較好,但程式設計師必須十分確定傳進去的值是可以進行正確轉換的,否則程式會直接panic。

如果要將建立出來的列舉設成「公開的」,可以在列舉名稱前加上「pub」關鍵字。例如:

#[macro_use]
extern crate enum_ordinalize;

create_ordinalized_enum!(pub MyEnum,
    Zero,
    One,
    Two
);

另外,如果想自訂序數的值,可以直接用「變體名稱 = 值」的語法來定義列舉。例如:

#[macro_use]
extern crate enum_ordinalize;

create_ordinalized_enum!(MySpecialEnum,
    Two = 2,
    Four = 4,
    Eight = 8
);

assert_eq!(2, MySpecialEnum::Two.ordinal());
assert_eq!(Some(MySpecialEnum::Four), MySpecialEnum::from_ordinal(4));
assert_eq!(MySpecialEnum::Eight, unsafe{MySpecialEnum::from_ordinal_unsafe(8)});

預設被的序數化的列舉的序數型別是isize,如果想要更改成其它整數型別的話,只要在宣告列舉時明確加上要改成的整數型別即可。例如:

#[macro_use]
extern crate enum_ordinalize;

create_ordinalized_enum!(MyEnum: u8,
    Zero,
    One,
    Two
);

assert_eq!(2u8, MyEnum::Two.ordinal());
assert_eq!(Some(MyEnum::One), MyEnum::from_ordinal(1u8));