聊一聊Rust的enum

2023-12-13 15:00:37

enum在實際程式設計中是非常常用的,enum的目的就是為了清晰定義出散落在系統各個角落的相同概念的有限固定值。

一、enum介紹
如果是簡單定義固定值,我們可以使用常數const。比如

public const int MAX_THREAD_COUNT=100;

在C語言中,我們可以這樣定義一個列舉方便各處使用,比如:

enum Direction
{
Left,
Center,
Right
}

C#基本繼承了C的enum性質,簡單無別的,比如:

public enum Week{
  Mon,Tue,Wed,Thu,Fri,Sat,Sun
}

當然可以加點其它,比起C要好一丟丟,然而也僅限於此。以至於當這種簡單型別無法滿足我們需要要擴充套件的時候就會使用class/struct來取代寫出類似這種程式碼

public sealed class Error
{
  public static readonly SignError=new Error(110,"sign error.");
  public static readonly NetworkError=new Error(500,"network is disconnected.");
  
  public int code{get;private set;}
  public string message{get;private set;}
  
  public Error(int code,string message)
  {
    this.code=code;
    this.message=message;
  }
}

這也是C#的enum雞肋的地方。當然這並不是列舉了,只不過到達了相似效果。

接著我們來看Java的enum,就會發現它比較好一些了。還拿上面這個例子來說,比如:

public enum Error
{
  SignError(110,"sign error."),NetworkError(500,"network is disconnected.");
  
  private int code;
  private String message;
  
  private Error(int code,String message)
  {
    this.code=code;
    this.message=message;
  }
}

這麼來看C#的變通enum和Java的原生enum能滿足我們大多數的使用場景。

在rust中我們也可以宣告類C這樣的enum,比如:

pub enum GameState{
Wait,Running,Stop,Reboot
}

rust的enum功效不止於此,我們來看看rust的enum的奇特之處。

二、變體enum(可以當有限泛型用-個人理解)
我們可以把不同資料型別放進一個enum裡,比如:

pub enum DbParameterValue<'a> {
  Null,
  I8(i8),
  U8(u8),
  I16(i16),
  U16(u16),
  I32(i32),
  U32(u32),
  I64(i64),
  U64(u64),
  F32(f32),
  F64(f64),
  String(&'a str),
  StringArray(&'a [&'a str]),
  U8Array(&'a [u8]),
}
pub DbParameter<'a>{
  pub name:&'a str,
  pub value:&'a DbParameterValue<'a>
}
pub DbSql<'a>{
  pub db_parameters: Vec<DbParameter<'a>>,
  ...
}

  可以看到rust的enum可以支援不同型別,以此到達泛型的效果,我個人把這個稱作是有限泛型。
  如果是C#或者Java要實現這個,就只能轉換成object,這樣必然觸發拆箱裝箱操作。

三、與match完美配合

let value=DbParameterValue::U8(100);
match value{
  DbParameterValue::U8(v)=>println!("{v}"),
  ...
}

  rust的解構操作可以直接取到實際的值,是不是很優雅。

  其實整個rust體系有兩個非常重要的enum:Result和Option。除了所有權和借用規則外,這兩個也是保障rust安全的法寶。我們來看看它的定義

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

  Ok(T)表示成功,並且包含返回值, T表示正確的返回值的型別(T為泛型);Err(E)表示失敗,並且包含了返回值,E表示錯誤的返回值的型別(泛型)。

enum Option<T> {
    None,
    Some(T),
}

  Some(T)表示元組結構體,封裝了一個 T 型別的值,None表示無。
  比如下面這個函數:

pub fn divide(a:i32,b:i32)->Result<i32,std::io::Error>{
    if b==0{
      Err(std::io::Error::new(std::io::ErrorKind::Other, "the divisor must not be ( zero )."))
    } else{
      Ok(a/b)
    }
}

pub fn try_divide(a:i32,b:i32)->Option<i32>{
    if b==0{
      None
    } else{
      Some(a/b)
    }
}

fn main(){
  if let Some(try_value)=try_divide(20,100){
    println!("try_value is {try_value}");
  }
  
  let value=divide(25,0);
  match value{
    Ok(value)=>println!("{value}"),
    Err(e)=>panic!("value is {:?}", error)
  }
}

  從呼叫可以看出,我們在實際函數呼叫的時候經常都必須針對result和option進行處理,這也就是為什麼rust沒有空指標的原因,所以說rust從根上是記憶體安全的。當然社群針對result有更好的解決方式(?操作符配合anyhow)。網上很多例子會unwrap();但是請不要這樣做,我們要保持漂亮的程式碼就應該儘量不使用unwrap,並且標準庫中也基本使用?操作符。

四、enum的記憶體佈局

看起來enum這麼好,有沒有缺點呢,缺點當然是有的,我們寫個例子來看看

use std::mem::{size_of,size_of_val};

enum PValue<'a>{
U8(u8),
U64(u64),
String(&'a str),
}
fn main(){
  println!("size of PValue:{}",size_of::<PValue>());
  println!("size of PValue::u8:{}",size_of_val(&PValue::U8(100)));
  println!("size of PValue::u64:{}",size_of_val(&PValue::U64(100)));
  println!("size of PValue::str:{}",size_of_val(&PValue::String("hello rust,here's my first rust")));
}

  輸出結果

size of PValue:24
size of PValue::u8:24
size of PValue::u64:24
size of PValue::str:24

  

可以看出來,enum的記憶體大小是以其中最大項的記憶體來分配的。enum每一項都會有1byte的tag分配,當然rust編譯器也有特殊優化,比如針對Option<T>就做了優化,捨棄了1byte的tag分配。

One more thing

很久沒寫文章了,都有點生疏了,以後還是要多練練,我發現後臺插入程式碼居然沒有rust語言可選,希望越來越好吧。

etermparser一個個人開源專案,屬於個人興趣。
https://github.com/bmrxntfj/eterm-parser
由於工作需要解析航信eterm系統返回的資料,目前部分實現了av,detr,fd,ml,pat,pnr等指令的結果解析。
有興趣可以聊聊交個朋友^_^。