聊一聊Rust的enum

发布时间 2023-12-13 14:05:59作者: bmrxntfj

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 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等指令的结果解析。
有兴趣可以聊聊交个朋友^_^。