如何在不克隆字符串的情况下在 Rust 中构建灵活的多类型数据系统?



我想建立一个系统,其中不同类型的数据(i32String,...(在修改数据的函数之间流动。例如,我想有一个add函数来获取"一些"数据并添加它。

add函数获取类型为Value的东西,如果Valuei32,它会添加两个i32值,如果它是String类型,则返回一个组合两个字符串的字符串。

我知道这对于模板编程来说几乎是完美的(或者不管在 Rust 中叫什么,我来自 C++(,但就我而言,我希望有处理这些东西的小代码块。

例如,使用f64String,使用FloatText作为名称,我有:

pub struct Float {
pub min: f64,
pub max: f64,
pub value: f64,
}
pub struct Text {
pub value: String,
}
pub enum Value {
Float(Float),
Text(Text),
}

现在我想实现一个函数,该函数获取一个应该是字符串的值并对其执行某些操作,因此我实现了Valueto_string()方法:

impl std::string::ToString for Value {
fn to_string(&self) -> String {
match self {
Value::Float(f) => format!("{}", f.value).to_string(),
Value::Text(t) => t.value.clone(),
}
}
}

现在该函数将执行以下操作:

fn do_something(value: Value) -> Value {
let s = value.to_string();
// do something with s, which probably leads to creating a new string
let new_value = Text(new_string);
Value::Text(new_value)
}

Value::Float的情况下,这将创建一个新的String,然后创建一个带有结果的新String并返回它,但在Value::Text的情况下,这将克隆字符串,这是一个不必要的步骤,然后创建一个新的。

有没有办法让to_string()实现可以在Value::Float上创建一个新String,但返回Value::Text值的引用?

处理String&str可能性的"标准"方法是使用Cow<str>。COW 代表写入时克隆(或写入时复制(,您可以将其用于字符串以外的其他类型。Cow允许您保存引用或拥有的值,并且仅在需要更改引用时才将引用克隆为拥有值。

有几种方法可以将其应用于代码:

  1. 您可以只添加一个Into<Cow<str>>实现并保持其余部分不变。
  2. 更改类型以始终保存Cow<str>,以允许Text对象保存拥有的String&str

第一个选项是最简单的。您可以只实现该特征。请注意,Into::into接受self,因此您需要为&Value而不是Value实现这一点,否则借用的值将引用已被into使用并且已经无效的拥有值。

impl<'a> Into<Cow<'a, str>> for &'a Value {
fn into(self) -> Cow<'a, str> {
match self {
Value::Float(f) => Cow::from(format!("{}", f.value).to_string()),
Value::Text(t) => Cow::from(&t.value),
}
}
}

通过实现这一点&'a Value,我们可以将Cow<'a, str>中的生存期与数据源联系起来。如果我们只为Value实施,这是不可能的,这很好,因为数据会消失!


更好的解决方案可能是在Text枚举中使用Cow

use std::borrow::Cow;
pub struct Text<'a> {
pub value: Cow<'a, str>,
}

这将让您持有借来的&str

let string = String::From("hello");
// same as Cow::Borrowed(&string)
let text = Text { value: Cow::from(&string) };

String

// same as Cow::Owned(string)
let text = Text { value: Cow::from(string) };

由于Value现在可以间接保存引用,因此它将需要自己的生命周期参数:

pub enum Value<'a> {
Float(Float),
Text(Text<'a>),
}

现在,Into<Cow<str>>实现可以用于Value本身,因为可以移动引用的值:

impl<'a> Into<Cow<'a, str>> for Value<'a> {
fn into(self) -> Cow<'a, str> {
match self {
Value::Float(f) => Cow::from(format!("{}", f.value).to_string()),
Value::Text(t) => t.value,
}
}
}

就像String一样,Cow<str>满足Deref<Target = str>,因此只需传递引用即可在任何需要&str的地方使用。这就是为什么您应该始终尝试在函数参数中接受&str而不是String&String的另一个原因。


通常,您可以像使用Strings 一样方便地使用Cows,因为它们具有许多相同的impl。例如:

let input = String::from("12.0");
{
// This one is borrowed (same as Cow::Borrowed(&input))
let text = Cow::from(&input);
}
// This one is owned (same as Cow::Owned(input))
let text = Cow::from(input);
// Most of the usual String/&str trait implementations are also there for Cow
let num: f64 = text.parse().unwrap();

最新更新