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?
Answers
The feedback you received is based on a more idiomatic way of handling references in Rust. Specifically, it is generally better to accept references to slices or trait objects rather than references to specific containers like Vec
or Box
. The reasons are tied to flexibility and generality, making your functions more broadly useful. Here's a detailed explanation:
Using References to String
and Vec
Directly
Let's examine your functions:
fn awesome_greeting(name: &String) {
println!("Wow, you are awesome, {}!", name);
}
fn total_price(prices: &Vec<i32>) -> i32 {
prices.iter().sum()
}
fn is_even(value: &Box<i32>) -> bool {
**value % 2 == 0
}
While these functions are correct, they can be made more flexible by accepting more general types.
Preferred Idiomatic Approach
Using &str
instead of &String
For string-like data, it's more idiomatic to use &str
:
fn awesome_greeting(name: &str) {
println!("Wow, you are awesome, {}!", name);
}
- Flexibility: This allows you to pass both
String
and&str
(string slices) directly to the function. - Efficiency:
&str
is a dynamically sized type, representing a slice of a string, so it can refer to part of aString
, a string literal, or any other data that can be viewed as a string.
You can call awesome_greeting
with a String
, &String
, or &str
:
let name1 = String::from("Alice");
let name2 = "Bob";
awesome_greeting(&name1);
awesome_greeting(name2);
Using Slices for Collections
For collections, it's more idiomatic to use slices (e.g., [T]
):
fn total_price(prices: &[i32]) -> i32 {
prices.iter().sum()
}
- Flexibility: This allows you to pass both
Vec<i32>
and&[i32]
(slices) to the function. - Efficiency: Slices are more general and allow the function to work with any contiguous sequence of elements, whether they are stored in a
Vec
, an array, or another type of collection that can be viewed as a slice.
You can call total_price
with a Vec<i32>
or a slice:
let prices_vec = vec![1, 2, 3];
let prices_slice: &[i32] = &[4, 5, 6];
total_price(&prices_vec);
total_price(prices_slice);
Using &T
instead of &Box<T>
For boxed values, it's more idiomatic to use &T
directly:
fn is_even(value: &i32) -> bool {
*value % 2 == 0
}
- Flexibility: This allows you to pass both
i32
values directly andBox<i32>
,Rc<i32>
, orArc<i32>
(or any other type that can dereference toi32
) to the function. - Efficiency: Using
&T
is more flexible because it doesn't tie the function to a specific smart pointer type.
You can call is_even
with a Box<i32>
or an i32
reference:
let value_box = Box::new(4);
let value = 6;
is_even(&value_box);
is_even(&value);
Conclusion
Using references to more general types like &str
, &[T]
, and &T
makes your functions more flexible and idiomatic in Rust. It allows your functions to work with a wider variety of input types, making them more reusable and easier to integrate into different parts of your codebase. This approach aligns better with Rust's emphasis on zero-cost abstractions and efficient, flexible code.