Rust中enum和String的互转

任何语言中,enumString两个类型的互转,都是基本操作, 因为String广泛地使用于各个有可能的人机接口和(跨语言或跨环境)机机接口。

Rust中的互转难题

假设我有个值,本质上是个字符串,但只能是几个固定值。 比方说,速度单位km/hm/s。 为了能很好地表达这个事,用enum是最合适的。

struct Sth {
    unit: SpeedUnit
}

enum SpeedUnit {
    Kmph,
    Mps,
}

然而,SpeedUnit::Kmphkm/h之间,如何实现互相转换,则是个较大的问题。 而且,这里的键和值是难以保持一致的。

  1. Rust命名规范中要求enum对象采用大驼峰方式命名,值不一定满足。
  2. 有些字符串中包含非法字符,如这里的/

自己实现

如果要自己动手实现from_strto_str,当然是可行的。

use std::str::FromStr;

#[derive(PartialEq)]
pub enum SpeedUnit {
    Kmph,
    Mps,
}

impl FromStr for SpeedUnit {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let unit = match s {
            "km/h" => SpeedUnit::Kmph,
            "m/s" => SpeedUnit::Mps,
            _ => return Err(s.to_string()),
        };
        Ok(unit)
    }
}

impl ToString for SpeedUnit {
    fn to_string(&self) -> String {
        let ret = match self {
            SpeedUnit::Kmph => "km/h",
            SpeedUnit::Mps => "m/s",
        };
        ret.to_string()
    }
}

但是,这种实现的代价很大。

要解决这种问题,在Rust中需要用宏。

略。

不写了,不太会。直接看strum_macros源码吧。

用strum_macros

use strum_macros::{Display, EnumString};

#[derive(Display, EnumString, PartialEq)]
enum SpeedUnit {
    #[strum(serialize = "km/h")]
    Kmph,
    #[strum(serialize = "m/s")]
    Mps,
}

strum_macrosDisplayEnumString,可以很好地满足这个互转要求。 基本上可以理解为,它利用Rust宏,把原先3个代码块才能实现的功能,集中到了1个代码块。

除了互转之外,strum_macros还支持其它偏门或常见的enum需求,详见文档

测试代码

上文展示的两种实现,都可以通过以下测试用例。 这证明它们在这些使用场景下是等价的。

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use rstest::rstest;

    use super::*;

    #[rstest]
    #[case("km/h", SpeedUnit::Kmph)]
    #[case("m/s", SpeedUnit::Mps)]
    fn test_speed_unit_parse(#[case] input: &str, #[case] expected: SpeedUnit) {
        let unit = SpeedUnit::from_str(input).unwrap();
        assert!(unit == expected);
    }

    #[rstest]
    #[case("kmh")]
    #[case("ms")]
    #[should_panic]
    fn test_speed_unit_parse_fail(#[case] input: &str) {
        SpeedUnit::from_str(input).unwrap();
    }

    #[rstest]
    #[case(SpeedUnit::Kmph, "km/h")]
    #[case(SpeedUnit::Mps, "m/s")]
    fn test_speed_unit_unparse(#[case] unit: SpeedUnit, #[case] expected: &str) {
        let value = unit.to_string();
        assert_eq!(value, expected)
    }
}

注意:两段实现中都加入了PartialEq,是为了使用第一个用例中的unit == expected。 如果实际使用中没有类似判断相等的需求,可以不加PartialEq。 如果需要还使用assert_eq!,则需要加Debug

附Cargo.toml

[dependencies]
strum = "0.24"
strum_macros = "0.24"

[dev-dependencies]
rstest = "0.15"

其中,strumstrum_macros都是必须的,而且版本要一致。 而rstest,则是支持本文测试代码中使用的case功能,可选添加。

另附Python的实现

from enum import Enum


class SpeedUnit(str, Enum):
    KMPH = 'km/h'
    MPS = 'm/s'


assert SpeedUnit('km/h') is SpeedUnit.KMPH
assert SpeedUnit.MPS.value == 'm/s'

显然,Python更简单。

但这是有语法糖支持的情况下。 Rust能通过宏,自由地进行元编程,潜在的表现力更大。

其它语言……


相关笔记