&str
, String
, AsRef<str>
, or Into<String>
in Rust?I'm developing a public Rust library and I'm unsure how to handle function parameters that deal with strings.
There seem to be multiple idiomatic options:
&str
String
impl AsRef<str>
impl Into<String>
Each of these works in different cases, but I don't understand when to use one over the others, especially when weighing ergonomics and API design vs. performance.
For example, if I have:
fn greet(name: &str) { ... }
fn greet<T: AsRef<str>>(name: T) { ... }
fn greet<T: Into<String>>(name: T) { ... }
fn greet(name: String) { ... }
Which case is more idiomatic, and in what situations?
&str
- String SliceUse this when you need to read the string (or parts of it), but don't need to own it.
let slice = "bob";
greet_str(slice);
let owned = String::new("bob");
greet_str(&owned); // Requires explicit borrowing of `owned`
fn greet_str(name: &str) {
println!("{}", name);
}
Pros:
Cons:
greet_str(owned)
will throw a compile-time error)String
- Owned StringUse this when you need to write or take ownership of the string, want to give power to the caller on how and when the string is allocated, and want to be explicit.
let slice = "bob";
greet_string(slice.to_string()); // Requires explicit allocation of `slice`
let owned = String::new("bob");
greet_string(owned); // Takes ownership of `owned`
fn greet_string(name: String) {
println!("{}", name);
}
Pros:
String
Cons:
&str
and clone if they want to keep an owned copyAsRef<str>
- Flexible for BorrowingUse this when you want to accept anything that borrows as a string (implements AsRef<str>
), and you don't need to take ownership.
let slice = "bob";
greet_asref(slice);
let owned = String::new("bob");
greet_asref(owned); // Notice how we don't have to explicitly borrow `owned`
fn greet_asref<T: AsRef<str>>(name: T) {
println!("Hello {}", name.as_ref())
}
Pros:
&str
, String
and other string-like types (e.g. Cow<'_, str>
) without the need for explicit dereferencingCons
impl Into<String>
- Flexible for OwnershipUse this when you need to take ownership of the string but want to be flexible about what can be passed to the function (anything that implements Into<String>
).
let slice = "bob";
greet_into(slice); // The allocation/cloning is done for us inside the function when `.into()` is called
let owned = String::new("bob");
greet_into(owned); // Since we're passing ownership to the function, the string will not be reallocated inside the function
fn greet_into<T: Into<String>>(name: T) {
let owned_name = name.into(); // This will only allocate a new string if `name` isn't owned
println!("Hello {}", owned_name);
}
Pros:
String
and &str
, allocating a String
for &str
automaticallyCons:
&str
)String
directlygreet_into(String::new("bob").as_str())
There isn't a definitive catch-all answer to this question, it really is up to your specific requirements. If you want to be more explicit about what your functions expect, &str
and String
are probably more appropriate for you. However, if flexibility and ergonomics are a priority, AsRef<str>
and Into<String>
are also suitable.
There shouldn't be a major difference in performance between accepting the trait vs. concrete types. Where performance issues could arise at scale is if the caller mistakenly clones or causes a clone when it's not necessary (I'd imagine this would be most common with Into<String>
). However, at a small-scale (not inside loops) and in a non-embedded environment, a few accidental clones likely won't cause issues.