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

Hel*_*got 2 string polymorphism type-conversion rust borrowing

我想构建一个系统,其中不同类型的数据 ( 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),
}
Run Code Online (Sandbox Code Playgroud)

现在我想实现一个函数来获取一个应该是一个字符串的值并对其做一些事情,所以我实现了以下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(),
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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)
}
Run Code Online (Sandbox Code Playgroud)

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

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

Pet*_*all 5

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

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

  1. 您只需添加一个Into<Cow<str>>实现,其余部分保持不变。
  2. 将您的类型更改为始终持有Cow<str>s,以允许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),
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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


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

use std::borrow::Cow;

pub struct Text<'a> {
    pub value: Cow<'a, str>,
}
Run Code Online (Sandbox Code Playgroud)

这会让你持有借来的&str

let string = String::From("hello");

// same as Cow::Borrowed(&string)
let text = Text { value: Cow::from(&string) };
Run Code Online (Sandbox Code Playgroud)

或者一个String

// same as Cow::Owned(string)
let text = Text { value: Cow::from(string) };
Run Code Online (Sandbox Code Playgroud)

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

pub enum Value<'a> {
    Float(Float),
    Text(Text<'a>),
}
Run Code Online (Sandbox Code Playgroud)

现在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,
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

就像String,Cow<str>满足Deref<Target = str>所以它可以在任何需要 a 的地方使用&str,只需传递一个引用。这是您应该始终&str在函数参数中尝试 accept而不是Stringor 的另一个原因&String


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

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();
Run Code Online (Sandbox Code Playgroud)