Question

Why Rust Avoids &String, &Vec, and &Box in Function Arguments

Question

I wrote some Rust code that accepts &String as a function parameter:

fn awesome_greeting(name: &String) {
    println!("Wow, you are awesome, {}!", name);
}

I have also written functions that take references to Vec and Box values:

fn total_price(prices: &Vec<i32>) -> i32 {
    prices.iter().sum()
}

fn is_even(value: &Box<i32>) -> bool {
    **value % 2 == 0
}

I was told that using &String, &Vec<T>, or &Box<T> as function arguments is usually discouraged in Rust. Why is that considered a poor API choice, and what types should be used instead?

Short Answer

By the end of this page, you will understand why Rust APIs usually prefer &str over &String, &[T] over &Vec<T>, and &T over &Box<T>. You will learn how these choices make functions more flexible, easier to call, and more idiomatic in real Rust code.

Concept

In Rust, function parameters should usually describe what the function needs, not how the caller stores the data.

That is the main reason &String, &Vec<T>, and &Box<T> are often discouraged.

The core idea

These types are all owned container types:

  • String owns heap-allocated text
  • Vec<T> owns a growable list of values
  • Box<T> owns a heap-allocated single value

When you write a function parameter like:

fn awesome_greeting(name: &String)

you are saying:

  • the caller must specifically have a String
  • and you only want a reference to that exact container type

But most functions do not actually need a String. They only need string data they can read. Rust already has a more general type for that:

&str

The same applies to vectors and boxed values:

  • if you only need to read a sequence, use a slice: &[T]
  • if you only need to read a value, use &T

Why this matters

Using the more general borrowed type makes your function:

  • more flexible
  • easier to call
  • more reusable
  • more idiomatic

For example:

fn awesome_greeting(name: &str) {
    println!("Wow, you are awesome, {}!", name);
}

Now this function can accept:

  • a string literal: "Alice"
  • a String: &my_string
  • a substring slice
  • many other string-like values after coercion

If you keep &String, you unnecessarily restrict callers.

Recommended replacements

Strings

Prefer:

fn awesome_greeting(name: &str)

instead of:

fn awesome_greeting(name: &String)

Vectors

Prefer:

fn total_price(prices: &[i32]) -> i32

instead of:

fn total_price(prices: &Vec<i32>) -> i32

A slice works with:

  • Vec<T>
  • arrays like [1, 2, 3]
  • parts of arrays or vectors such as &numbers[1..3]

Box

Prefer:

fn is_even(value: &i32) -> bool

instead of:

fn is_even(value: &Box<i32>) -> bool

If the function only needs to read the i32, it should take &i32, not a reference to the box that owns it.

A useful rule

Ask yourself:

Does this function care about the ownership container, or only about the borrowed view of the data?

Most of the time, it only cares about the borrowed view.

That is why Rust style strongly prefers borrowed forms like &str, &[T], and &T.

Mental Model

Think of this like asking to read a book.

  • String, Vec<T>, and Box<T> are like specific kinds of containers: a hardcover book, a storage box, or a folder.
  • &str, &[T], and &T are like asking for the content itself: the text on the page, the list of items, or the value inside.

If you only need to read the content, it is too restrictive to say:

  • "I will only accept a hardcover book"

when what you really mean is:

  • "I just need the text"

That is what &String does: it asks for a specific owned string container.

That is what &str does better: it asks for readable string data.

So the mental model is:

  • use owned types when you need ownership
  • use borrowed view types when you only need access
  • prefer the most general type that matches what your function actually needs
Take Quiz

Syntax and Examples

Preferred function signatures

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
}

Why these are better

  • &str accepts string slices and String references through coercion
  • &[i32] accepts vectors, arrays, and sub-slices
  • &i32 accepts a reference to an integer, including one stored inside a Box<i32>

Example usage

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
}

fn main() {
    let name = String::from("Taylor");
    awesome_greeting(&name);
    awesome_greeting("Jordan");
    awesome_greeting(&name[0..3]);

    let prices_vec = vec![10, 20, 30];
    let prices_array = [5, 15, 25];

    println!("{}", total_price(&prices_vec));
    println!("{}", total_price(&prices_array));
    println!("{}", total_price(&prices_vec[1..]));

    let boxed = Box::new(8);
    let plain = 6;

    println!("{}", is_even(&boxed));
    println!("{}", is_even(&plain));
}

What is happening with Box

&boxed has type &Box<i32>, but Rust can dereference it to &i32 when needed. That means a function taking &i32 is more useful than one taking &Box<i32>.

Step by Step Execution

Consider this example:

fn total_price(prices: &[i32]) -> i32 {
    prices.iter().sum()
}

fn main() {
    let items = vec![10, 20, 30];
    let result = total_price(&items);
    println!("{}", result);
}

Step by step

1. Create the vector

let items = vec![10, 20, 30];

items is a Vec<i32>. It owns the numbers.

2. Borrow it for the function call

total_price(&items)

&items is a reference to the vector, but the function expects &[i32].

Rust can coerce &Vec<i32> into &[i32] because a vector can be viewed as a slice of its contents.

3. Inside the function

fn total_price(prices: &[i32]) -> i32 {
    prices.iter().sum()
}

Now prices is just a borrowed slice.

  • it does not own the data
  • it cannot grow or shrink the vector
  • it can read the values

4. Iterate over the slice

prices.iter()

This creates an iterator over 10, 20, and 30.

5. Sum the values

.sum()

The iterator adds the values and returns 60.

6. Print the result

println!("{}", result);

The output is:

60

Important takeaway

The function did not need a vector specifically. It only needed a readable sequence of integers. That is why &[i32] is the better parameter type.

Real World Use Cases

1. Processing text input

A logging or validation function usually only needs to read text:

fn validate_username(name: &str) -> bool {
    !name.trim().is_empty() && name.len() >= 3
}

This can work with:

  • String
  • string literals
  • slices of strings

2. Reading lists of data

A statistics function often only needs a sequence:

fn average(values: &[i32]) -> f32 {
    let sum: i32 = values.iter().sum();
    sum as f32 / values.len() as f32
}

This works with vectors, arrays, and parts of collections.

3. Utility functions over values

A function checking a number does not care whether the number lives on the stack or heap:

fn is_positive(value: &i32) -> bool {
    *value > 0
}

4. API and library design

Library authors choose flexible parameter types so users can call functions from many contexts without extra conversions.

5. Data parsing and transformation

When parsing input, borrowed views avoid unnecessary constraints:

fn parse_id(text: &str) -> Result<u32, std::num::ParseIntError> {
    text.parse()
}

This is more ergonomic than requiring &String.

Take Quiz

Real Codebase Usage

In real Rust codebases, developers usually design function parameters around capability.

Common patterns

Read-only text input

Use &str when the function only reads text.

fn log_message(message: &str) {
    println!("LOG: {}", message);
}

Read-only sequence input

Use &[T] when the function only reads a list.

fn contains_zero(values: &[i32]) -> bool {
    values.iter().any(|v| *v == 0)
}

Generic read-only value access

Use &T when the function only needs the value.

fn print_number(n: &i32) {
    println!("{}", n);
}

Guard clauses and validation

Flexible borrowed types are especially useful in validation code:

fn ensure_not_empty(input: &str) -> Result<(), &'static str> {
    if input.trim().is_empty() {
        return Err("input cannot be empty");
    }
    Ok(())
}

Working with iterators and collection pipelines

Slices integrate nicely with iterator methods:

fn count_even(values: &[i32]) -> usize {
    values.iter().filter(|n| **n % 2 == 0).count()
}

Public API design

For public functions, using &str and &[T] reduces friction:

  • fewer conversions for callers
  • less unnecessary coupling to concrete container types
  • better reuse across modules and crates

This is one of the most common signs of idiomatic Rust API design.

Common Mistakes

Mistake 1: Taking &String when &str is enough

Broken design:

fn greet(name: &String) {
    println!("Hello, {}", name);
}

Problem:

  • cannot accept a string literal directly
  • unnecessarily tied to String

Better:

fn greet(name: &str) {
    println!("Hello, {}", name);
}

Mistake 2: Taking &Vec<T> instead of a slice

Overly specific:

fn sum_items(items: &Vec<i32>) -> i32 {
    items.iter().sum()
}

Problem:

  • does not naturally express "any readable sequence"
  • less flexible than a slice

Better:

fn sum_items(items: &[i32]) -> i32 {
    items.iter().sum()
}

Mistake 3: Taking &Box<T> when only &T is needed

Too specific:

fn print_value(value: &Box<i32>) {
    println!("{}", value);
}

Problem:

  • the function does not care that the value is heap-allocated
  • forces callers into a boxed representation

Better:

fn print_value(value: &i32) {
    println!("{}", value);
}

Mistake 4: Confusing ownership with borrowing

Sometimes beginners think &String means “borrowed text”, but in Rust there is a more appropriate borrowed text type: &str.

Remember:

  • String = owned growable string
  • &str = borrowed string slice

Mistake 5: Using container types in APIs without a reason

Only use &Vec<T> or &String> if your function truly needs methods or guarantees specific to that owned container.

For example, if a function needs vector capacity information, then &Vec<T> may be justified:

fn print_capacity(values: &Vec<i32>) {
    println!("capacity = {}", values.capacity());
}

This is valid because slices do not have capacity().

How to avoid these mistakes

Before writing a parameter type, ask:

  • Do I need ownership?
  • Do I need this exact container type?
  • Or do I only need read access to the contents?

Choose the least specific type that still expresses the function's needs.

Comparisons

NeedToo specificPreferredWhy
Read text&String&strAccepts more kinds of string data
Read a list&Vec<T>&[T]Works with vectors, arrays, and slices
Read a value&Box<T>&TDoes not care how the value is stored

&String vs &str

TypeMeaningBest use
&StringReference to a String objectRarely needed
&strBorrowed string dataMost read-only string parameters

&Vec<T> vs &[T]

TypeMeaningBest use
&Vec<T>Reference to a vectorWhen vector-specific features are needed
&[T]Borrowed sequence sliceMost read-only list parameters

&Box<T> vs &T

TypeMeaningBest use
&Box<T>Reference to a heap-owning boxRarely needed
&TBorrowed valueMost read-only value parameters

When a specific container is appropriate

Sometimes the more specific type is correct.

Examples:

  • use String if your function must take ownership of text
  • use Vec<T> if your function must grow, sort, or consume the vector
  • use Box<T> if ownership of boxed allocation matters

The rule is not "never use these types". The rule is "do not require them unless you truly need them."

Take Quiz

Cheat Sheet

Quick rules

  • Prefer &str over &String
  • Prefer &[T] over &Vec<T>
  • Prefer &T over &Box<T>
  • Use owned types only when ownership is required
  • Use the least specific type your function needs

Common patterns

fn read_text(s: &str) {}
fn read_items(items: &[i32]) {}
fn read_value(x: &i32) {}

When to use owned types

fn takes_ownership(name: String) {}
fn consumes_vector(values: Vec<i32>) {}
fn stores_boxed_value(value: Box<i32>) {}

Why slices and borrowed views help

  • more flexible APIs
  • easier function calls
  • better reuse
  • more idiomatic Rust

Exceptions

Use &String, &Vec<T>, or &Box<T> only if you need behavior specific to those exact types.

Examples:

fn string_capacity(s: &String) -> usize {
    s.capacity()
}

fn vec_capacity(v: &Vec<i32>) -> usize {
    v.capacity()
}

Useful reminder

Ask:

  • Do I need the container?
  • Or just the data inside it?

If you just need the data, prefer the borrowed view type.

FAQ

Why is &str preferred over &String in Rust?

Because &str is more general. It can represent borrowed string data from string literals, String, and string slices.

Is &Vec<T> ever wrong?

Not always. It is fine if you truly need vector-specific behavior such as capacity(). But for read-only access to elements, &[T] is usually better.

Why is &Box<T> usually unnecessary?

Because most functions only care about the value T, not that it is stored in a Box. A parameter of type &T is usually enough.

Does using &str or &[T] improve performance?

The main benefit is API flexibility and clarity. It also avoids unnecessary constraints, and borrowed views are efficient.

Can I still pass a String to a function that takes &str?

Yes. Rust can coerce &String to &str in many common cases.

Can I still pass a Vec<T> to a function that takes &[T]?

Yes. Rust can coerce &Vec<T> to &[T].

Should I always avoid concrete types in parameters?

No. Use concrete types when your function genuinely depends on them. Otherwise, prefer more general borrowed forms.

Take Quiz

Mini Project

Description

Build a small set of utility functions for a command-line shopping and greeting program. The project demonstrates how to design flexible Rust function signatures using &str, slices, and plain references instead of &String, &Vec<T>, and &Box<T>. This is useful because real Rust code often needs helper functions that can work with many kinds of inputs without forcing callers into one specific container type.

Goal

Create utility functions that accept borrowed views of data so they can work with string literals, String, arrays, vectors, and boxed values.

Requirements

  • Write a greeting function that accepts any readable string input.
  • Write a function that sums a list of prices using a slice.
  • Write a function that checks whether a number is even using a plain reference.
  • Call the functions with different input types such as String, string literals, arrays, vectors, and Box<i32>.
  • Print the results to confirm the functions work correctly.
Take Quiz

Keep learning

Related questions

View more