RustArena

Step 2 of 6

Borrow, don't clone

Every .clone() of a String or Vec allocates new heap memory and copies every byte. One clone costs nanoseconds, but a clone inside a loop that runs a million times is a million allocations. Needless clones and .to_string() calls are the first thing reviewers look for in Rust performance work, and usually the easiest to remove.

Take &str and &[T] when you only read

A parameter of type String or Vec<T> takes ownership, so a caller who wants to keep their value has to clone it first. If the function only reads, borrow: &str accepts string literals, &String and slices of either; &[T] accepts arrays, &Vec<T> and slices.

#[derive(Debug)]
struct User {
    name: String,
    country: String,
}

// Borrows everything: no allocation at all.
fn find_user<'a>(users: &'a [User], name: &str) -> Option<&'a User> {
    users.iter().find(|u| u.name == name)
}

// Only the results are cloned, not every user.
fn names_in(users: &[User], country: &str) -> Vec<String> {
    users.iter().filter(|u| u.country == country).map(|u| u.name.clone()).collect()
}

fn main() {
    let users = vec![
        User { name: "ana".to_string(), country: "PT".to_string() },
        User { name: "bo".to_string(), country: "SE".to_string() },
        User { name: "cy".to_string(), country: "PT".to_string() },
    ];
    println!("{:?}", find_user(&users, "bo").map(|u| &u.country));
    println!("{:?}", names_in(&users, "PT"));
}
Some("SE")
["ana", "cy"]

When owning is right

If the function stores the value (in a struct field, a map, a channel), take it by value: fn add_tag(&mut self, tag: String). Then the caller decides: move a String they no longer need (free), or clone one they still need. Cloning inside the function would force a copy even when the caller didn't need one.

Common mistakes

clippy helps: unnecessary_to_owned (on by default), needless_pass_by_value (in the opt-in clippy::pedantic group) and redundant_clone (in the opt-in clippy::nursery group) flag many of these.

Your task

orders_for takes ownership of everything and clones on every step. Change it to fn orders_for<'a>(orders: &'a [Order], customer: &str) -> Vec<&'a Order>: borrow the orders, borrow the name, and return references to the matching orders, with no clones or string conversions at all.

Challenge 2

longest_word takes a String and returns a new String, cloning along the way. Change it to fn longest_word(text: &str) -> &str, returning a slice of the input. If several words tie for longest, return the first. For text with no words, return "".

Challenge 3

Write count_errors, which counts the log lines that start with error in any ASCII case (ERROR disk full, Error: timeout, error) without allocating: no to_lowercase, no new Strings. Take the first 5 bytes with line.get(..5) (it returns None if the line is shorter or byte 5 isn't a character boundary) and compare with eq_ignore_ascii_case. Option::is_some_and(|x| ...) is a handy way to test the value inside an Option in one call.

Previous: Measure first, and fix the algorithm first Next: Allocate less