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

How to build a flexible multiple type data system in Rust without cloning strings?

我想构建一个系统,让不同类型的数据(i32String、...)在修改数据的函数之间流动。例如,我想要一个 add 函数来获取 "some" 数据并将其添加。

add 函数获取类型为 Value 的内容,如果 Value 是一个 i32,它会添加两个 i32 值,如果它是String 类型,它 return 是一个结合了两个字符串的字符串。

我知道这对于模板编程来说几乎是完美的(或者在 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),
}

现在我想实现一个函数来获取一个应该是字符串的值并对它做一些事情,所以我为Value实现了to_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 结果和 return 它,但是在a Value::Text 这将克隆字符串,这是不必要的步骤,然后创建新字符串。

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

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

有几种方法可以将其应用到您的代码中:

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

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

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 的地方,只需传递一个引用。这是 而不是 String&String.

的另一个原因

一般来说,您可以像使用String一样方便地使用Cow,因为它们有许多相同的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();