Strings in most native or performance focused languages tend to present a fair amount of complexity and Rust is no exception to this. There are cases where you have a struct that needs to have a name, such as:
struct ServiceHandle {
name: String,
}
The first ServiceHandle::new() function I would typically write when first learning Rust would look like this:
fn new(name: String) -> ServiceHandle {
// other init stuff
ServiceHandle { name: name }
}
In the real world I generally have a String to pass to ServiceHandle::new(String) so this API works well. But when I’m writing tests for my code, I want to pass hard coded values with type &'static str. In order to do that, I have to call one of the conversion functions.
ServiceHandle::new("listener".into());
ServiceHandle::new("listener".to_owned());
ServiceHandle::new(String::from("listener"));
If I change the signature to something like:
fn new(name: &str) -> ServiceHandle {
// other init stuff
ServiceHandle { name: name.to_owned() }
}
Then I have to remember to prefix String with & when passing to the function. Another possibility is to from_string and from_str which is what I started to rely on next. Then I don’t have to remember as much, just use the right one for the right type.
fn from_str(name: &str) -> ServiceHandle {
// skip init stuff, let from_string do it
ServiceHandle::from_string(name.to_owned())
}
fn from_string(name: String) -> ServiceHandle {
// other init stuff
ServiceHandle { name: name }
}
This gets me into a better state where it is easy to remember what string type it takes, but it feels clumsy. Rust provides the From and Into traits that types can implement to enable more generic coding. They are as their name implies a way to automatically change types. They are “reflexive” which means that if one of them is implemented the other can use it. For example, impl From<A> for B would allow you to write a function like fn thing<T: Into<B>>(arg: T) which could be called like thing(A {}).
So the next iteration of my ServiceHandle::new() went generic:
fn new<S: Into<String>>(name: S) -> ServiceHandle {
ServiceHandle { name: name.into() }
}
This allows calling with String, &str or several other types that can automatically be converted into a String. Making writing testing code with &'static str simple, while dynamically generated String objects still a first class citizen.
This is something I’ve struggled with myself. I’ve tended to favor `String` often when I shouldn’t just because it’s so much more simple to pass around with the lack of lifetime concerns, but I feel like this is a crutch of sorts. Using Into seems like the only sane way to avoid times where you are switching from owned to slice and back again just to satisfy different signatures as data flows through your program.
Lately, I’ve favored using `&str` in my parameter lists since learning that there is automatic conversion of `&String` to `&str` (likely via a trait mechanic as you’ve shown). I guess this is just shorthand for `.as_ref()` but I find it convenient.