Why &String, &Vec, and &Box are bad practice in Rust?

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: The trap every Rust beginner falls into

\n\n

You are learning Rust. You've already figured out the concept of ownership and borrowing. You're writing your first serious function and think: "Aha, to avoid moving the string, I'll pass a reference!". And you write:

\n\n
fn print_string(s: &String) {\n    println!("{}", s);\n}
\n\n

The compiler is happy. The code works. You are happy. But here's the trouble: an experienced Rust developer, looking at your code, will wince. Why? After all, it works!

\n\n

This question is one of the most popular on StackOverflow. Thousands of developers stumble over this seemingly insignificant detail. Today we'll figure out why &String, &Vec, and &Box in a function signature are a red flag, and how to write correctly.

\n\n

Problem #1: Implicit type restriction (Type Coercion)

\n\n

Rust has a powerful system of automatic type coercion, but only in one direction. This is called Deref Coercion.

\n\n

The gist is simple: if you have a reference to String, Rust can automatically turn it into a reference to &str. The same goes for &Vec<T> → &[T] and &Box<T> → &T.

\n\n

Look at the difference:

\n\n
// Bad: only accepts &String\nfn bad(s: &String) {\n    println!("{}", s);\n}\n\n// Good: accepts &str\nfn good(s: &str) {\n    println!("{}", s);\n}\n\nfn main() {\n    let owned = String::from("Hello, world!");\n    let slice = "Hello, slice!";\n    \n    bad(&owned); // Works\n    // bad(slice); // ERROR! slice is &str, not &String\n    \n    good(&owned); // Works (automatic coercion)\n    good(slice);   // Works (it's already &str)\n}
\n\n

See? The good function accepts any string: both the owner String, a string literal &str, and even a part of a string. The bad function only accepts String. This is an unnecessary restriction.

\n\n

Practical example: substring search

\n\n
// Restricted search\nfn find_in_string(haystack: &String, needle: &str) -> bool {\n    haystack.contains(needle)\n}\n\n// Universal search\nfn find_in_str(haystack: &str, needle: &str) -> bool {\n    haystack.contains(needle)\n}\n\nfn main() {\n    let my_string = String::from("Rust is awesome");\n    let my_str = "Rust is awesome";\n    \n    // Only works with String\n    find_in_string(&my_string, "Rust");\n    // find_in_str(my_str, "Rust"); // Compilation error!\n    \n    // Works with both\n    find_in_str(&my_string, "Rust");\n    find_in_str(my_str, "Rust");\n}
\n\n

Problem #2: Performance and unnecessary indirection

\n\n

Let's look "under the hood". What is String in memory? It's a structure with three fields:

\n
    \n
  • Pointer to data on the heap
  • \n
  • String length
  • \n
  • Capacity
  • \n
\n\n

When you pass &String, you pass a reference to this structure. To get to the data, the compiler needs to:

\n
    \n
  1. Dereference &String → get the String structure
  2. \n
  3. From the structure, take the pointer to the data on the heap
  4. \n
  5. Dereference that pointer → get the string bytes
  6. \n
\n\n

When you pass &str, you pass a fat pointer, which consists of just two fields:

\n
    \n
  • Pointer to data
  • \n
  • Length
  • \n
\n\n

The compiler needs to make only one step — dereference the pointer. Less indirection means faster code. The difference is microscopic, but in hot loops or when passing to

Blogs

Book Recommendations