Why is it discouraged to accept a reference &String, &Vec, or &Box as a function argument?
I wrote some Rust code that takes a &String
as an argument:
fn awesome_greeting(name: &String) {
println!("Wow, you are awesome, {}!", name);
}
I've also written code that takes in a reference to a Vec
or Box
:
fn total_price(prices: &Vec<i32>) -> i32 {
prices.iter().sum()
}
fn is_even(value: &Box<i32>) -> bool {
**value % 2 == 0
}
However, I received some feedback that doing it like this isn't a good idea. Why not? **
Answer**
The feedback you received is likely based on the fact that your function signatures are using references to owned types (&String
, &Vec
, and &Box
) instead of references to the underlying data (&str
, &[i32]
, and &i32
). In Rust, it's considered more idiomatic to accept references to the inner data (i.e., the actual data without the ownership part) whenever possible. Here's why:
1. &String
vs &str
&String
is a reference to an ownedString
type, which itself allocates its own heap-allocated buffer for the string data.&str
is a reference to a string slice, which can refer to a part of aString
or even a string literal. A&str
is generally more flexible and lighter because it doesn't require the overhead of owning the string.
Problem with &String
:
When you pass a &String
, you're passing a reference to an owned object (String
), which means you're also carrying around the ownership semantics. This is typically unnecessary if you don't need the specific features of String
, such as mutation or ownership of the string. If all you're doing is reading the string, you should prefer a &str
, which is more efficient and general-purpose.
Fix:
fn awesome_greeting(name: &str) {
println!("Wow, you are awesome, {}!", name);
}
This way, your function can accept both &String
and &str
, since String
can be dereferenced to &str
automatically. You also don't impose unnecessary overhead by using a reference to an owned object when a simpler reference to the underlying data is sufficient.
2. &Vec<i32>
vs &[i32]
&Vec<i32>
is a reference to an ownedVec<i32>
, which is a dynamically-allocated, heap-allocated vector containingi32
values.&[i32]
is a reference to a slice ofi32
values. A slice is a lightweight view into a sequence of data, and it can refer to data owned by aVec
or even a static array.
Problem with &Vec<i32>
:
Passing a &Vec<i32>
means you're accepting a reference to an entire Vec
, which again carries ownership semantics that aren't needed for just accessing the data. You don't care about the Vec
's ownership; you just want to access the contents. Using a slice (&[i32]
) avoids the unnecessary overhead.
Fix:
fn total_price(prices: &[i32]) -> i32 {
prices.iter().sum()
}
A &[i32]
is cheaper to pass around, more flexible, and works for any kind of sequence of i32
s, not just a Vec<i32>
.
3. &Box<i32>
vs &i32
&Box<i32>
is a reference to a boxed value. ABox<i32>
is an owned heap-allocated value, which can be dereferenced to get the underlyingi32
.&i32
is a reference to ani32
value, and it is much more direct and efficient if you're just dealing with the data.
Problem with &Box<i32>
:
Using &Box<i32>
is unnecessary unless you explicitly want to work with the Box
type (e.g., for some ownership or heap-specific behavior). You don’t need to pass a reference to the Box
when you can simply pass a reference to the underlying value (&i32
).
Fix:
fn is_even(value: &i32) -> bool {
*value % 2 == 0
}
Now, your function is directly accepting a reference to the data itself, and the caller can pass in a reference to any i32
, not just one wrapped in a Box
.
4. Summary
&String
vs&str
: Use&str
when you just need a string slice.&String
should be used only when you need ownership or need to mutate the string.&Vec<i32>
vs&[i32]
: Use&[i32]
for slices, as it's more general and avoids the unnecessary overhead of referencing an ownedVec<i32>
.&Box<i32>
vs&i32
: Use&i32
for a reference to the data.&Box<i32>
is usually unnecessary unless you're working with ownership semantics specifically.
The key idea is to prefer references to borrowed data (&str
, &[i32]
, &i32
) over references to owned types (&String
, &Vec<i32>
, &Box<i32>
) when you're just reading the data, as it avoids unnecessary ownership-related semantics and makes the code simpler, more efficient, and more flexible.