DEV Community

Gregory Chris
Gregory Chris

Posted on

Why &str is Better Than String in Parameters

Why &str is Better Than String in Parameters: Simplify APIs with Borrowed Strings

When you're writing Rust code, one of the first design questions you'll face is how to handle strings in your function signatures. Should you accept String, or should you use &str? This seemingly small decision can have a big impact on the usability, performance, and clarity of your code.

If you're new to Rust, you might wonder why this decision even matters. After all, both String and &str are used to represent text, right? Well, not quite. In this blog post, we’ll explore why you should almost always prefer &str in function parameters and reserve String for when it’s truly necessary. By the end, you’ll not only understand the "why" but also the "how," complete with practical examples and tips to avoid common pitfalls.


The String vs &str Dilemma

To start, let’s clarify the difference between String and &str in Rust:

  • String: This is an owned, growable heap-allocated string. It owns the memory it uses to store its data and can be modified.
  • &str: This is a borrowed string slice, typically referencing a string stored elsewhere. It’s immutable and does not own the data it points to.

Here’s a quick example to illustrate:

fn main() {
    let owned: String = String::from("Hello, world!"); // Owned string
    let borrowed: &str = &owned; // Borrowed string slice

    println!("{}", owned);    // "Hello, world!"
    println!("{}", borrowed); // "Hello, world!"
}
Enter fullscreen mode Exit fullscreen mode

At first glance, this might seem like a trivial distinction. But under the hood, it has significant implications for performance, memory management, and API design.


Why &str is Better Than String in Parameters

1. Flexibility: Accept More String-Like Inputs

When your function accepts &str as a parameter, it becomes more versatile because &str can easily reference many types of string-like data:

  • String literals ("hello")
  • Borrowed slices of String
  • String slices (&my_string[0..5])
  • Results of .as_str() and .to_string() methods

Here’s a simple example:

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

fn main() {
    let owned = String::from("Alice");
    let literal = "Bob";

    greet(&owned); // Borrow a `String`
    greet(literal); // Use a string literal
}
Enter fullscreen mode Exit fullscreen mode

If the greet function had accepted String instead, we’d have to clone or transfer ownership each time, making it less ergonomic:

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

fn main() {
    let owned = String::from("Alice");
    let literal = "Bob";

    greet(owned); // Moves the `String`
    // greet(literal); // Error: can't pass a string literal directly
}
Enter fullscreen mode Exit fullscreen mode

With &str, we can handle both owned and borrowed strings seamlessly, making our APIs simpler and easier to use.


2. Performance: Avoid Unnecessary Cloning

Passing a String by value requires ownership transfer, which can trigger a heap allocation and copy. On the other hand, passing a &str simply involves passing a reference, which is lightweight and efficient.

Let’s compare the two approaches:

Using String:

fn count_chars(text: String) -> usize {
    text.len()
}

fn main() {
    let data = String::from("Hello, Rust!");

    // This clones the `String` to transfer ownership
    let length = count_chars(data.clone());
    println!("Length: {}", length);
}
Enter fullscreen mode Exit fullscreen mode

Here, the data.clone() call is necessary to prevent the original data from being moved. However, cloning is expensive, especially for large strings.

Using &str:

fn count_chars(text: &str) -> usize {
    text.len()
}

fn main() {
    let data = String::from("Hello, Rust!");

    // No cloning or ownership transfer needed
    let length = count_chars(&data);
    println!("Length: {}", length);
}
Enter fullscreen mode Exit fullscreen mode

By accepting &str, we avoid unnecessary allocations and keep our code efficient.


3. Align with Rust’s Ownership and Borrowing Philosophy

Rust’s ownership system encourages borrowing over owning whenever possible. Accepting &str in function arguments aligns with this philosophy, as it allows you to work with existing data without taking ownership or duplicating it.

Think of it like borrowing a book from a library: you can read it (immutable reference) without owning it. Returning the book (dropping the reference) doesn’t cost you anything extra.


When to Use String in Parameters

There are cases where accepting a String is appropriate:

  1. You Need Ownership: If your function must take ownership (e.g., to modify or store the string), then it makes sense to accept String. For instance:
   fn consume_string(s: String) {
       println!("Consumed: {}", s);
   }

   fn main() {
       let my_string = String::from("Goodbye!");
       consume_string(my_string); // Ownership is transferred
   }
Enter fullscreen mode Exit fullscreen mode
  1. You’re Returning a String: If your function generates a new string, it’s natural to return a String:
   fn generate_greeting(name: &str) -> String {
       format!("Hello, {}!", name)
   }

   fn main() {
       let greeting = generate_greeting("Alice");
       println!("{}", greeting);
   }
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls and How to Avoid Them

1. Forgetting to Borrow

When calling a function that expects &str, always remember to borrow the String:

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

fn main() {
    let name = String::from("Alice");

    // Correct: Borrow the `String`
    greet(&name);

    // Incorrect: Passing the `String` directly won't work
    // greet(name); // Error: expected `&str`, found `String`
}
Enter fullscreen mode Exit fullscreen mode

2. Unnecessary Cloning

Avoid cloning a String just to pass it to a function that accepts &str. Instead, borrow it:

fn process(input: &str) {
    println!("Processing: {}", input);
}

fn main() {
    let data = String::from("Rustacean");

    // Correct: Borrow the `String`
    process(&data);

    // Incorrect: Cloning is wasteful
    process(&data.clone());
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • Prefer &str in function parameters to make your APIs more flexible and efficient.
  • Reserve String for cases where ownership is explicitly required.
  • Borrowing strings (&str) avoids unnecessary memory allocations and aligns with Rust’s ownership model.
  • Use String for returning new strings or when you need to mutate or store them.

Next Steps

If you want to deepen your understanding, here are some great resources to explore:

By mastering the art of borrowing strings, you’ll write cleaner, faster, and more idiomatic Rust code. Happy coding!

Top comments (0)