Creating a Rust function that accepts String or &str
In mylast 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 useString
vs&str
in astruct
. I think this advice is good, but there are cases where using&str
instead ofString
is not optimal. We need another strategy for these use cases.
A struct Containing Strings
Consider thePerson
struct below. For the sake of discussion, let's sayPerson
has a real need to own thename
variable. We choose to use theString
type instead of&str
.
structPerson {name: String,}
Now we need to implement anew()
function. Based on my last blog post, we prefer a&str
:
implPerson {fnnew(name: &str) -> Person { Person { name: name.to_string() } }}
This works as long as we remember to call.to_string()
inside of thenew()
function. However, the ergonomics of this function are less than desired. If we use a string literal, then we can make a newPerson
likePerson.new("Herman")
. If we already have aString
though, we need to ask for a reference to theString
:
let name = "Herman".to_string();let person = Person::new(name.as_ref());
It feels like we are going in circles though. We had aString
, then we calledas_ref()
to turn it into a&str
only to then turn it back into aString
inside of thenew()
function. We could go back to using aString
likefn 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 theInto trait. This trait will can automatically convert a&str
into aString
. If we already have aString
, then no conversion happens.
structPerson {name: String,}implPerson {fnnew<S: Into<String>>(name: S) -> Person { Person { name: name.into() } }}fnmain() {let person = Person::new("Herman");let person = Person::new("Herman".to_string());}
This syntax fornew()
looks a little different. We are usingGenerics andTraits to tell Rust that some typeS
must implement the traitInto
for typeString
. TheString
type implementsInto<String>
as noop because we already have aString
. The&str
type implementsInto<String>
by using the same.to_string()
method we were originally doing in thenew()
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 usingInto<String>
hurts performance and the answer is no. Rust usesstatic dispatch and the concept ofmonomorphization to handle all this during the compiler phase.
Don't worry if things likestatic dispatch andmonomorphization are confusing. You just need to know that using the syntax above you can create functions that accept bothString
and&str
. If you are thinking thatfn 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 aboutInto<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 oncrates.io. All this userland power is what makes Rust an awesome language.
Another Way To Write Person::new()
Thewhere syntax also works and may be easier to read, especially if the function signature becomes more complex:
structPerson {name: String,}implPerson {fnnew<S>(name: S) -> Personwhere S: Into<String> { Person { name: name.into() } }}