DEV Community

Gregory Chris
Gregory Chris

Posted on

Avoiding Clones: Borrowing Smart in Rust

Avoiding Clones: Borrowing Smart in Rust

When you first start writing Rust, .clone() can feel like a lifesaver. Need to use a value after it's been moved? .clone() it. Want to pass something into a function without relinquishing ownership? .clone() it. But here’s the catch: .clone() isn’t free. Every time you call .clone(), you're creating a deep copy of your data, which can incur significant performance overhead.

If you're coming from languages with garbage collection, this might seem natural. But in Rust, borrowing is often the better (and faster) way to go. This post will teach you how to recognize when you don’t need .clone() and how to write efficient, idiomatic Rust code by mastering the art of borrowing.


Why .clone() Is a Tempting Trap

Before diving into borrowing, let’s understand why .clone() is so common for new Rustaceans. Rust’s ownership system ensures memory safety at compile time, but it also introduces constraints on how you can use values:

  • Ownership Transfer: When you pass ownership of a value to another part of your program, you can no longer use it unless it’s returned.
  • Move Semantics: For types that don’t implement the Copy trait, values are moved, not copied, when assigned or passed to functions.

Here’s an example of a common scenario:

fn print_and_return(input: String) -> String {
    println!("{}", input);
    input
}

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

    // This would fail:
    // println!("{}", my_string);
}
Enter fullscreen mode Exit fullscreen mode

In this example, my_string is moved into the print_and_return function. If you try to use it after the function call, the compiler will throw a "value borrowed after move" error. To avoid this, many developers instinctively reach for .clone():

let my_string = String::from("Hello, Rust!");
let returned_string = print_and_return(my_string.clone());

println!("{}", my_string); // Works, but at what cost?
Enter fullscreen mode Exit fullscreen mode

This works, but now you’ve made a deep copy of my_string, which involves allocating memory and copying data. For small strings, this might not seem like a big deal, but for larger data structures, the performance hit can be significant.


Borrowing: The Smarter Alternative

Borrowing allows you to give temporary access to a value without transferring ownership. This is achieved using references (&T) or mutable references (&mut T). Let’s refactor the previous example to use borrowing instead of cloning:

fn print_and_return(input: &String) {
    println!("{}", input);
}

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

    // Works without cloning!
    println!("{}", my_string);
}
Enter fullscreen mode Exit fullscreen mode

By passing a reference (&my_string) to print_and_return, we allow the function to read the value without taking ownership. After the function call, my_string is still valid in main.


Real-World Refactor: Borrowed vs Cloned Values

Let’s tackle a more complex example. Imagine we’re building a program that processes a list of tasks. Here’s an initial implementation using .clone():

struct Task {
    name: String,
    completed: bool,
}

fn mark_as_completed(tasks: Vec<Task>, task_name: String) -> Vec<Task> {
    tasks
        .into_iter()
        .map(|mut task| {
            if task.name == task_name {
                task.completed = true;
            }
            task
        })
        .collect()
}

fn main() {
    let tasks = vec![
        Task {
            name: "Learn Rust".to_string(),
            completed: false,
        },
        Task {
            name: "Write Blog Post".to_string(),
            completed: false,
        },
    ];

    let task_name = "Learn Rust".to_string();
    let updated_tasks = mark_as_completed(tasks.clone(), task_name.clone());

    println!("Original tasks: {:?}", tasks);
    println!("Updated tasks: {:?}", updated_tasks);
}
Enter fullscreen mode Exit fullscreen mode

This code works, but notice how we’re cloning both the tasks vector and the task_name string. Let’s refactor it to use borrowing instead:

Refactored Version

struct Task {
    name: String,
    completed: bool,
}

fn mark_as_completed(tasks: &[Task], task_name: &str) -> Vec<Task> {
    tasks
        .iter()
        .map(|task| {
            let mut updated_task = task.clone();
            if task.name == task_name {
                updated_task.completed = true;
            }
            updated_task
        })
        .collect()
}

fn main() {
    let tasks = vec![
        Task {
            name: "Learn Rust".to_string(),
            completed: false,
        },
        Task {
            name: "Write Blog Post".to_string(),
            completed: false,
        },
    ];

    let task_name = "Learn Rust";
    let updated_tasks = mark_as_completed(&tasks, task_name);

    println!("Original tasks: {:?}", tasks);
    println!("Updated tasks: {:?}", updated_tasks);
}
Enter fullscreen mode Exit fullscreen mode

What Changed?

  1. Borrowed the tasks Vector: Instead of taking ownership of tasks, we pass a reference (&[Task]) to mark_as_completed. This avoids cloning the entire vector.
  2. Borrowed the task_name String: Instead of taking ownership of task_name, we use a string slice (&str).

Why Is This Better?

  • Performance: No unnecessary clones for tasks or task_name.
  • Memory Safety: Rust ensures that the borrowed values (tasks and task_name) remain valid for the duration of their usage.
  • Clarity: Borrowing clearly communicates intent: the function doesn’t need ownership, only access.

Common Pitfalls and How to Avoid Them

1. Forgetting to Use References in Loops

Consider this example:

let numbers = vec![1, 2, 3, 4, 5];
for num in numbers {
    println!("{}", num);
}

// This would fail because `numbers` is moved!
println!("{:?}", numbers);
Enter fullscreen mode Exit fullscreen mode

Solution: Borrow the values in the loop.

for num in &numbers {
    println!("{}", num);
}
println!("{:?}", numbers);
Enter fullscreen mode Exit fullscreen mode

2. Overusing .clone() in Function Arguments

If a function doesn’t require ownership, use references:

fn print_value(value: &String) {
    println!("{}", value);
}
Enter fullscreen mode Exit fullscreen mode

Instead of this:

fn print_value(value: String) {
    println!("{}", value);
}
Enter fullscreen mode Exit fullscreen mode

3. Mutable References and Borrowing Rules

Rust enforces strict borrowing rules: you can have either one mutable reference or multiple immutable references, but not both. This can trip up developers:

let mut value = String::from("Hello");
// let ref1 = &value;
// let ref2 = &mut value; // Error: cannot borrow as mutable because it is also borrowed as immutable
Enter fullscreen mode Exit fullscreen mode

Solution: Ensure references don’t overlap in scope.


Key Takeaways

  • Borrowing is Powerful: Use references (&T/&mut T) to avoid unnecessary clones and improve performance.
  • Think Ownership: Before calling .clone(), ask yourself: does this function need ownership, or just access?
  • Avoid Pitfalls: Pay attention to borrowing rules, especially when dealing with mutable references.

Next Steps for Learning

  1. Practice Borrowing: Refactor existing code to replace .clone() with references where possible.
  2. Explore Lifetimes: Learn how lifetimes work to manage complex borrowing scenarios.
  3. Dive Deeper into Ownership: Read the Rust Book’s chapter on ownership for a deeper understanding.

Borrowing smartly in Rust is more than just avoiding .clone(). It’s about writing code that’s efficient, idiomatic, and takes full advantage of Rust’s ownership model. Happy coding!

Top comments (0)