Why is it discouraged to accept a reference &String, &Vec, or &B

ghz 21days ago ⋅ 19 views

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 owned String 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 a String 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 owned Vec<i32>, which is a dynamically-allocated, heap-allocated vector containing i32 values.
  • &[i32] is a reference to a slice of i32 values. A slice is a lightweight view into a sequence of data, and it can refer to data owned by a Vec 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 i32s, not just a Vec<i32>.

3. &Box<i32> vs &i32

  • &Box<i32> is a reference to a boxed value. A Box<i32> is an owned heap-allocated value, which can be dereferenced to get the underlying i32.
  • &i32 is a reference to an i32 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 owned Vec<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.