Reputation: 369
In Rust I would like to give a name to a class, and this class owns this name.
Sometimes the name is passed by an String
. For this case I can just simply move the ownership.
But sometimes this name is given by a static string(&str
).
For this case I want to refer to that string, rather than making a String
from it.
My question is: how can I declare this name field in my class? What type should it be?
Some updates/background on the requirements:
String
and a &str
is that I want to reduce the dynamic allocation to the minimum.Upvotes: 1
Views: 329
Reputation: 155465
One option is to declare the name member as an enum that can either contain a String
or an &'static str
:
enum Name {
Static(&'static str),
Owned(String),
}
struct Class {
name: Name,
// ...
}
The class can then provide appropriate constructors (there will have to be two) and a get_name()
method for accessing the name as string slice:
impl Class {
pub fn new_from_str(name: &'static str) -> Class {
Class { name: Name::Static(name) }
}
pub fn new_from_owned(name: String) -> Class {
Class { name: Name::Owned(name) }
}
pub fn get_name(&self) -> &str {
match self.name {
Name::Owned(ref s) => s.as_str(),
Name::Static(s) => s,
}
}
}
fn main() {
let c1 = Class::new_from_str("foo");
let c2 = Class::new_from_owned("foo".to_string());
println!("{} {}", c1.get_name(), c2.get_name());
}
The other option is to use the Cow
type provided by the standard library for this purpose:
use std::borrow::Cow;
struct Class {
name: Cow<'static, str>,
}
Since Cow
implements the Into
trait, the constructor can now be written as a single generic function:
pub fn new<T>(name: T) -> Class
where T: Into<Cow<'static, str>> {
Class { name: name.into() }
}
Cow
also implements the Deref
trait, allowing get_name()
to be written as:
pub fn get_name(&self) -> &str {
return &self.name;
}
In both cases the name
member will equal the size of the larger variant plus the space taken by the discriminator. As String
is the larger type here, and it takes up three pointer sizes (the string contents is allocated separately and doesn't count), Name
will take four pointer sizes in total. In case of explicit enum
, the member can be made smaller still by boxing the string:
enum Name {
Static(&'static str),
Owned(Box<String>),
}
This will cut down the size of Name
to three pointer sizes, of which one slot is used for the discriminator and the remaining two for the string slice. The downside is that it requires an additional allocation and indirection for the owned-string case - but it might still pay off if the majority of your class names come from static string slices.
Upvotes: 4
Reputation: 33380
What you can do to allow for a mix of types is use the Into
trait. This is versatile in that it makes sure a safe conversion happens between the types.
A str
slice can be converted "into" an owned String
as such.
Some test code to demonstrate it:
#[derive(Debug)]
struct Test {
name: String,
}
impl Test {
pub fn new<T: Into<String>>(t: T) -> Test {
Test { name: t.into() }
}
}
fn main() {
let t = Test::new("a");
println!("{:?}", t);
}
Upvotes: 0