I want to build a system where data of different types (i32
, String
, ...) flows between functions that modify the data. For example, I want to have an add
function that gets "some" data and adds it.
The add
function gets something of type Value
and if Value
is an i32
, it adds the two i32
values, if it is of type String
, it returns a string that combines both strings.
I know that this would be almost perfect for template programming (or whatever this is called in Rust, I'm coming from C++) but in my case I want to have small code blocks that handle the stuff.
As an example, with f64
and String
, using Float
and Text
as the names, I have:
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),
}
Now I want to implement a function that gets a value that is supposed to be a string and does something to it, so I implement the to_string()
method for 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(),
}
}
}
Now the function would do something like:
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)
}
In the case of a Value::Float
this would create a new String
, then a new String
with the result and return it, but in the case of a Value::Text
this would clone the string, which is an unnecessary step, and then create the new one.
Is there a way where the to_string()
implementation could create a new String
on Value::Float
but return the reference of Value::Text
's value?
The "standard" way to deal with the possibility of either a
String
or a&str
is to use aCow<str>
. COW stands for clone-on-write (or copy-on-write) and you can use it for other types besides strings. ACow
lets you hold either a reference or an owned value, and only clone a reference into an owned value when you need to mutate it.There are a couple of ways you can apply this to your code:
Into<Cow<str>>
implementation and keep the rest the same.Cow<str>
s throughout, to allowText
objects to hold either an ownedString
or a&str
.The first option is easiest. You can just implement the trait. Note that the
Into::into
acceptsself
, so you need to implement this for&Value
notValue
, otherwise the borrowed values would be referencing owned values that have been consumed byinto
and are already invalid.Implementing this for
&'a Value
lets us tie the lifetime in theCow<'a, str>
back to the source of the data. This wouldn't be possible if we implemented just forValue
which is good because the data would be gone!An even better solution might be to use
Cow
in yourText
enum too:This will let you hold a borrowed
&str
:Or a
String
:Since
Value
now can indirectly hold a reference, it will need a lifetime parameter of its own:Now the
Into<Cow<str>>
implementation can be forValue
itself because referenced values can be moved:Just like
String
,Cow<str>
satisfiesDeref<Target = str>
so it can be used anywhere that a&str
is expected, by just passing a reference. This is another reason why you should always try accept&str
in a function argument, rather thanString
or&String
.Generally, you can use
Cow
s as conveniently asString
s, because they have many of the sameimpl
s. For example: