Rust tip and trick — one string for all Link to heading
Have you ever been frustrated with 2 different variants of String types: &str vs String? Yes, they are different in that &str is a reference to a String or str while String is the dynamically allocated string object. But in many cases, we tend to interchange them and just wish Rust is not too picky on which type I supply. For example, consider (Rust playground link)
use std::time::{SystemTime, UNIX_EPOCH};
fn time_stamp(msg: &str) -> String {
let time = SystemTime::now()
.duration_since(UNIX_EPOCH).unwrap().as_millis();
time.to_string() + ": " + msg
}
fn main() {
let s1 = "msg as &str";
let s2 = String::from("msg as String");
println!("{}", time_stamp(s1));
println!("{}", time_stamp(&s2)); // notice extra `&` character
}
Since the function time_stamp() takes &str type as the parameter, we must explicitly convert String type into the reference with & character; otherwise, we will get Rust compilation error. Wouldn’t it be so nice if Rust can automatically handle either type and do what is necessary? Well, it can! Rather than specifying the concrete argument type &str , we can specify a trait bound AsRef<str>:
--- before
+++ after
@@ -3 +3 @@
-fn time_stamp(msg: &str) -> String {
+fn time_stamp(msg: impl AsRef<str>) -> String {
@@ -6 +6 @@
- time.to_string() + ": " + msg
+ time.to_string() + ": " + msg.as_ref()
@@ -13 +13 @@
- println!("{}", time_stamp(&s2)); // notice extra `&` character
+ println!("{}", time_stamp(s2));
Now, we can pass any type that implements AsRef<str> to the function, such as &str or String! This is especially useful when you are designing an API for a library, as it makes the client code much easier to write.
Be aware though. You don’t want to use this trick for every situation. Consider the function below. Do you see any issue?
fn concat(msg1: impl AsRef<str>, msg2: impl AsRef<str>) -> String {
msg1.as_ref().to_owned() + "; " + msg2.as_ref()
}
The function concat is doing unnecessary work when the supplied type for msg1 is already a String, because it is calling to_owned() method on msg1 regardless. Ideally, we want to re-use msg1 if it is already of type String. For that, you can use another trait bound Into<String> instead:
--- before
+++ after
@@ -1,2 +1,2 @@
-fn concat(msg1: impl AsRef<str>, msg2: impl AsRef<str>) -> String {
- msg1.as_ref().to_owned() + "; " + msg2.as_ref()
+fn concat(msg1: impl Into<String>, msg2: impl AsRef<str>) -> String {
+ msg1.into() + "; " + msg2.as_ref()
In summary, use AsRef<str> trait bound if the preferred type is &str, and use Into<String> trait bound if the preferred type is String. Moreover, as long as you are careful not to execute unnecessary work as in the previous example, there will be no performance penalty — the compiler will generate specific code for the provided argument types, analogous to function templates in C++!