首页 > 解决方案 > 如何在不克隆字符串的情况下在 Rust 中构建灵活的多类型数据系统?

问题描述

我想构建一个系统,其中不同类型的数据(i32, String, ...)在修改数据的函数之间流动。例如,我想要一个add函数来获取“一些”数据并添加它。

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

我知道这对于模板编程几乎是完美的(或者在 Rust 中调用的任何东西,我来自 C++),但在我的情况下,我想要处理这些东西的小代码块。

例如,使用f64and String,使用FloatandText作为名称,我有:

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),
}

现在我想实现一个函数,它获取一个应该是字符串的值并对它做一些事情,所以我实现了以下to_string()方法Value

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)
}

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

有没有一种方法可以让to_string()实现创建一个新String的 onValue::Float但返回Value::Text' 值的引用?

标签: stringrusttype-conversionpolymorphismborrowing

解决方案


处理 aString或 a可能性的“标准”方法&str是使用 a Cow<str>。COW 代表写时克隆(或写时复制),您可以将其用于字符串以外的其他类型。ACow允许您保存引用或拥有的值,并且仅在需要对其进行变异时将引用克隆为拥有的值。

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

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

第一个选项是最简单的。您可以只实现该特征。请注意,Into::intoaccepts self,因此您需要为&Valuenot实现此功能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>与数据源联系起来。如果我们只执行 which 是好的,这是不可能的Value,因为数据会消失!


一个更好的解决方案也可能是Cow在您的Text枚举中使用:

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) };

由于Valuenow 可以间接持有引用,因此它需要自己的生命周期参数:

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预期 a 的地方使用,只需传递一个引用。这也是为什么您应该始终尝试&str在函数参数中接受,而不是Stringor的另一个原因&String


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

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();

推荐阅读