我想建立一个系统,其中不同类型的数据(i32
,String
,...(在修改数据的函数之间流动。例如,我想有一个add
函数来获取"一些"数据并添加它。
add
函数获取类型为Value
的东西,如果Value
是i32
,它会添加两个i32
值,如果它是String
类型,则返回一个组合两个字符串的字符串。
我知道这对于模板编程来说几乎是完美的(或者不管在 Rust 中叫什么,我来自 C++(,但就我而言,我希望有处理这些东西的小代码块。
例如,使用f64
和String
,使用Float
和Text
作为名称,我有:
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
并返回它,但在Value::Text
的情况下,这将克隆字符串,这是一个不必要的步骤,然后创建一个新的。
有没有办法让to_string()
实现可以在Value::Float
上创建一个新String
,但返回Value::Text
值的引用?
处理String
或&str
可能性的"标准"方法是使用Cow<str>
。COW 代表写入时克隆(或写入时复制(,您可以将其用于字符串以外的其他类型。Cow
允许您保存引用或拥有的值,并且仅在需要更改引用时才将引用克隆为拥有值。
有几种方法可以将其应用于代码:
- 您可以只添加一个
Into<Cow<str>>
实现并保持其余部分不变。 - 更改类型以始终保存
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
的另一个原因。
通常,您可以像使用String
s 一样方便地使用Cow
s,因为它们具有许多相同的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();