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:
Stringowns heap-allocated textVec<T>owns a growable list of valuesBox<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>, andBox<T>are like specific kinds of containers: a hardcover book, a storage box, or a folder.&str,&[T], and&Tare 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
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
&straccepts string slices andStringreferences through coercion&[i32]accepts vectors, arrays, and sub-slices&i32accepts a reference to an integer, including one stored inside aBox<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.
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
| Need | Too specific | Preferred | Why |
|---|---|---|---|
| Read text | &String | &str | Accepts more kinds of string data |
| Read a list | &Vec<T> | &[T] | Works with vectors, arrays, and slices |
| Read a value | &Box<T> | &T | Does not care how the value is stored |
&String vs &str
| Type | Meaning | Best use |
|---|---|---|
&String | Reference to a String object | Rarely needed |
&str | Borrowed string data | Most read-only string parameters |
&Vec<T> vs &[T]
| Type | Meaning | Best use |
|---|---|---|
&Vec<T> | Reference to a vector | When vector-specific features are needed |
&[T] | Borrowed sequence slice | Most read-only list parameters |
&Box<T> vs &T
| Type | Meaning | Best use |
|---|---|---|
&Box<T> | Reference to a heap-owning box | Rarely needed |
&T | Borrowed value | Most read-only value parameters |
When a specific container is appropriate
Sometimes the more specific type is correct.
Examples:
- use
Stringif 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."
Cheat Sheet
Quick rules
- Prefer
&strover&String - Prefer
&[T]over&Vec<T> - Prefer
&Tover&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.
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, andBox<i32>. - Print the results to confirm the functions work correctly.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.