Rust中enum和String的互转
2023-03-16 23:04:30 +08 字数:1144 标签: Rust任何语言中,enum
和String
两个类型的互转,都是基本操作,
因为String
广泛地使用于各个有可能的人机接口和(跨语言或跨环境)机机接口。
Rust中的互转难题 ¶
假设我有个值,本质上是个字符串,但只能是几个固定值。
比方说,速度单位km/h
和m/s
。
为了能很好地表达这个事,用enum
是最合适的。
struct Sth {
unit: SpeedUnit
}
enum SpeedUnit {
Kmph,
Mps,
}
然而,SpeedUnit::Kmph
和km/h
之间,如何实现互相转换,则是个较大的问题。
而且,这里的键和值是难以保持一致的。
- Rust命名规范中要求
enum
对象采用大驼峰方式命名,值不一定满足。 - 有些字符串中包含非法字符,如这里的
/
。
自己实现 ¶
如果要自己动手实现from_str
和to_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_macros的Display和EnumString,可以很好地满足这个互转要求。 基本上可以理解为,它利用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"
其中,strum和strum_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能通过宏,自由地进行元编程,潜在的表现力更大。
其它语言……