TL;DR: One can instead use &str
, &[T]
or &T
to allow for more generic code.
One of the main reasons to use a String
or a Vec
is because they allow increasing or decreasing the capacity. However, when you accept an immutable reference, you cannot use any of those interesting methods on the Vec
or String
.
Accepting a &String
, &Vec
or &Box
also requires the argument to be allocated on the heap before you can call the function. Accepting a &str
allows a string literal (saved in the program data) and accepting a &[T]
or &T
allows a stack-allocated array or variable. Unnecessary allocation is a performance loss. This is usually exposed right away when you try to call these methods in a test or a main
method:
awesome_greeting(&String::from("Anna"));
total_price(&vec![42, 13, 1337])
is_even(&Box::new(42))
Another performance consideration is that &String
, &Vec
and &Box
introduce an unnecessary layer of indirection as you have to dereference the &String
to get a String
and then perform a second dereference to end up at &str
.
Instead, you should accept a string slice (&str
), a slice (&[T]
), or just a reference (&T
). A &String
, &Vec<T>
or &Box<T>
will be automatically coerced (via deref coercion) to a &str
, &[T]
or &T
, respectively.
fn awesome_greeting(name: &str) {
println!("Wow, you are awesome, {}!", name);
}
fn total_price(prices: &[i32]) -> i32 {
prices.iter().sum()
}
fn is_even(value: &i32) -> bool {
*value % 2 == 0
}
Now you can call these methods with a broader set of types. For example, awesome_greeting
can be called with a string literal ("Anna"
) or an allocated String
. total_price
can be called with a reference to an array (&[1, 2, 3]
) or an allocated Vec
.
If you'd like to add or remove items from the String
or Vec<T>
, you can take a mutable reference (&mut String
or &mut Vec<T>
):
fn add_greeting_target(greeting: &mut String) {
greeting.push_str("world!");
}
fn add_candy_prices(prices: &mut Vec<i32>) {
prices.push(5);
prices.push(25);
}
Specifically for slices, you can also accept a &mut [T]
or &mut str
. This allows you to mutate a specific value inside the slice, but you cannot change the number of items inside the slice (which means it's very restricted for strings):
fn reset_first_price(prices: &mut [i32]) {
prices[0] = 0;
}
fn lowercase_first_ascii_character(s: &mut str) {
if let Some(f) = s.get_mut(0..1) {
f.make_ascii_lowercase();
}
}
In addition to Shepmaster's answer, another reason to accept a &str
(and similarly &[T]
etc) is because of all of the other types besides String
and &str
that also satisfy Deref<Target = str>
. One of the most notable examples is Cow<str>
, which lets you be very flexible about whether you are dealing with owned or borrowed data.
If you have:
fn awesome_greeting(name: &String) {
println!("Wow, you are awesome, {}!", name);
}
But you need to call it with a Cow<str>
, you'll have to do this:
let c: Cow<str> = Cow::from("hello");
// Allocate an owned String from a str reference and then makes a reference to it anyway!
awesome_greeting(&c.to_string());
When you change the argument type to &str
, you can use Cow
seamlessly, without any unnecessary allocation, just like with String
:
let c: Cow<str> = Cow::from("hello");
// Just pass the same reference along
awesome_greeting(&c);
let c: Cow<str> = Cow::from(String::from("hello"));
// Pass a reference to the owned string that you already have
awesome_greeting(&c);
Accepting &str
makes calling your function more uniform and convenient, and the "easiest" way is now also the most efficient. These examples will also work with Cow<[T]>
etc.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With