iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 3
0

Rust 中的列舉是 Rust 一個強大的功能,它跟 C 的列舉最大的不同是,它可以帶有資料

enum Foo {
  A(i32),
  B {
    a: u8,
    b: u64
  }
}

如果你有學過 C 語言,那你或許會知道 C 語言的列舉背後其實是個整數型態

enum Foo {
  A,
  B
}

printf("%d\n", A); // 應該會印出 0

但 Rust 的呢,絕對不是只是個整數那麼簡單而已,而其實在 C 之中也有類似的東西,那就是 union ,那才是唯一可以存進幾個不同型態的功能,像這樣:

union Foo {
  int A,
  struct { char a; int b; } B;
}

就可以做出類似 Rust 中的 enum 的功能了,只是還是不太一樣,在 C 中的 union 要自己記住實際存了什麼,不然的話萬一存取錯欄位可能會發生不可預期的行為,那如果我們把資料存在哪個欄位的資訊也存進去呢:

struct Foo {
  int tag;
  union {
    int A,
    struct { char a; int b; } B;
  } data;
}

比如有存資料的是 A 那就在 tag 存進 0 ,如果是 B 那就存進 1 ,透過多加一個變數,並在要進行存取前檢查這個變數,這樣就能減少出錯的可能性,而實際上 Rust 的 enum 就是以類似這樣的方式運作的,如果我們以上面的資料型態,再加上一個 main

enum Foo {
  A(i32),
  B {
    a: u8,
    b: u64
  }
}

fn main() {
  let a = Foo::A(3);
  let b = Foo::B { a: 1, b: 42 };
}

然後拿去用這個指令編成組語的話

$ rustc --emit=asm main.rs

我們可以在組語的 main 裡找到這段

  movl  $3, 4(%rsp)
  movb  $0, (%rsp)
  movb  $1, 17(%rsp)
  movq  $42, 24(%rsp)
  movb  $1, 16(%rsp)

對了,這個系列在某些地方會看到組語,或 llvm ir ,但不會詳細解釋每個指令在做什麼,因此希望你可以自己去了解

於是從這段組語中可以推斷出來 Rust 中的 enum 應該是底下的儲存方式

https://ithelp.ithome.com.tw/upload/images/20190918/20111802svUjRzu4kS.png

但如果你拿去跟上面的 C 語言產生的組語比較的話,你會發現在 padding 方面好像又不太一樣, Rust 為了提高空間的利用效率,實際上是用像這樣的方式儲存的:

union Foo {
  struct A {
    tag: u8,
    0: i32
  },
  struct B {
    tag: u8,
    a: u8,
    b: u64
  },
}

上面那個並不是正確的語法,只是這樣寫我比較好表示而已

像這樣把 tag 也放進去當 struct 的一個值,再加上 Rust 會重排 struct 內的值,這樣在某些情況下就能減少 padding 占的消耗

不過也因為這樣的特性,如何把 Rust 的 enum 與 C 的做轉換就是一大問題了,目前比較安全的做法就只有自己做轉換了

enum Foo {
  A(i32),
  B { a: u8, b: u64 },
}

#[derive(Copy, Clone)]
struct RawFooVariantB {
  a: u8,
  b: u64,
}

#[repr(C)]
union RawFooVariant {
  a: i32,
  b: RawFooVariantB,
}

#[repr(C)]
struct RawFoo {
  tag: u8,
  variant: RawFooVariant,
}

impl From<Foo> for RawFoo {
  fn from(foo: Foo) -> RawFoo {
    match foo {
      Foo::A(i) => RawFoo {
        tag: 0,
        variant: RawFooVariant { a: i },
      },
      Foo::B { a, b } => RawFoo {
        tag: 0,
        variant: RawFooVariant {
            b: RawFooVariantB { a, b },
        },
      },
    }
  }
}

目前這個問題有一些與之相關的 issue 與 rfc

另外這種結構,雖說可以在 C 或 C++ 中實作出來,可是因為 C 與 C++ 都沒有 pattern matching ,所以用起來並沒有像 Rust 這樣好用,如果在 C++ 或許還可以用指標與 class 的繼承來做出類似的東西會比較方便一點

參考資料


上一篇
Rust 簡介
下一篇
閉包 (Closure)
系列文
從 Rust 往程式底層前進26

尚未有邦友留言

立即登入留言