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"]
find_userreturnsOption<&'a User>: a reference into the caller's slice. Nothing is copied. The lifetime'asays the result borrows fromusers, not fromname.names_inhas to return owned names (the caller keeps them), but it clones only the matches, after filtering.u.name == namecompares aStringwith a&strdirectly; no conversion needed.
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
for x in list.clone()just to loop: writefor x in &list.- Converting to compare:
if word.to_lowercase() == "error"allocates a newStringfor every word.word.eq_ignore_ascii_case("error")allocates nothing. .iter().cloned().filter(...): clones everything, then throws most of it away. Filter first, clone the survivors.- Over-correcting: a clone of a short string at startup costs nothing that matters. Keep clones out of hot loops; elsewhere, readability wins. Cloning an
RcorArcis cheap (it just bumps a counter).
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