Creating a Rust function that accepts String or &str
In my last post we talked a lot about using &str
as the preferred type for functions accepting a string argument. Towards the end of that post there was some discussion about when to use String
vs &str
in a struct
. I think this advice is good, but there are cases where using &str
instead of String
is not optimal. We need another strategy for these use cases.
A struct Containing Strings
Consider the Person
struct below. For the sake of discussion, let's say Person
has a real need to own the name
variable. We choose to use the String
type instead of &str
.
struct Person {
name: String,
}
Now we need to implement a new()
function. Based on my last blog post, we prefer a &str
:
impl Person {
fn new (name: &str) -> Person {
Person { name: name.to_string() }
}
}
This works as long as we remember to call .to_string()
inside of the new()
function. However, the ergonomics of this function are less than desired. If we use a string literal, then we can make a new Person
like Person.new("Herman")
. If we already have a String
though, we need to ask for a reference to the String
:
let name = "Herman".to_string();
let person = Person::new(name.as_ref());
It feels like we are going in circles though. We had a String
, then we called as_ref()
to turn it into a &str
only to then turn it back into a String
inside of the new()
function. We could go back to using a String
like fn new(name: String) -> Person {
, but that means we need to force the caller to use .to_string()
whenever there is a string literal.
Into conversions
We can make our function easier for the caller to work with by using the Into trait. This trait will can automatically convert a &str
into a String
. If we already have a String
, then no conversion happens.
struct Person {
name: String,
}
impl Person {
fn new<S: Into<String>>(name: S) -> Person {
Person { name: name.into() }
}
}
fn main() {
let person = Person::new("Herman");
let person = Person::new("Herman".to_string());
}
This syntax for new()
looks a little different. We are using Generics and Traits to tell Rust that some type S
must implement the trait Into
for type String
. The String
type implements Into<String>
as noop because we already have a String
. The &str
type implements Into<String>
by using the same .to_string()
method we were originally doing in the new()
function. So we aren't side-stepping the need for the .to_string()
call, but we are taking away the need for the caller to do it. You might wonder if using Into<String>
hurts performance and the answer is no. Rust uses static dispatch and the concept of monomorphization to handle all this during the compiler phase.
Don't worry if things like static dispatch and monomorphization are confusing. You just need to know that using the syntax above you can create functions that accept both String
and &str
. If you are thinking that fn new<S: Into<String>>(name: S) -> Person {
is a lot of syntax, it is. It is important to point out though that there is nothing special about Into<String>
. It is just a trait that is part of the Rust standard library. You could implement this trait yourself if you wanted to. You can implement similar traits you find useful and publish them on crates.io. All this userland power is what makes Rust an awesome language.
Another Way To Write Person::new()
The where syntax also works and may be easier to read, especially if the function signature becomes more complex:
struct Person {
name: String,
}
impl Person {
fn new<S>(name: S) -> Person where S: Into<String> {
Person { name: name.into() }
}
}