4

Rust中enum和String的互转

 1 year ago
source link: https://note.qidong.name/2023/03/rust-enum-str/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Rust中enum和String的互转

2023-03-16 23:04:30 +08  字数:1144  标签: Rust

任何语言中,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()
    }
}

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

  • "km/h""m/s"被写了两次。为了避免重复,还需要定义一组常量。
  • 如果值域扩展,比如加一个"m/h",相关的三个代码块都需要配合修改。
  • 如果有很多这种enum,这些形式相同的代码,需要被反复写很多遍。

要解决这种问题,在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能通过宏,自由地进行元编程,潜在的表现力更大。

其它语言……


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK