2. Find a user by ID
The task
Section titled “The task”Find the user with a given ID in a list of users.
The versions
Section titled “The versions”WorseAn index loop over
&Vec<User>, a full copy of the user, and a panic when the ID is not in the list.pub fn find_user(users: &Vec<User>, id: u64) -> User {for i in 0..users.len() {if users[i].id == id {return users[i].clone();}}panic!("user {id} not found");}OKAn iterator and
Option: no panic. But.cloned()copies the user, with its strings, on every call.pub fn find_user(users: &[User], id: u64) -> Option<User> {users.iter().find(|user| user.id == id).cloned()}Better
Option<&User>: no panic and no copy. The caller decides if it needs its own copy.pub fn find_user(users: &[User], id: u64) -> Option<&User> {users.iter().find(|user| user.id == id)}
The “Worse” version is a translation of a Java method with a for loop that throws NoSuchElementException. Every line of it is a small problem:
&Vec<User>instead of&[User]limits what callers can pass. See S06:&String,&Vec<T>,&Box<T>parameters.for i in 0..users.len()withusers[i]is an index loop. It needs a bounds check on each access, and it is longer thaniter().find(...). See S10: Index-based loops instead of iterators.users[i].clone()copies the user and its two strings.panic!for a missing user. A missing user is a normal case, not a bug. See S36:panic!used for normal errors.
With Option<&User>, the caller handles the missing case in the type, and borrows the user that it found:
pub fn greeting(users: &[User], id: u64) -> String { match find_user(users, id) { Some(user) => format!("Hello, {}", user.name), None => "Hello, guest".to_string(), }}Verdict
Section titled “Verdict”| Criterion | OK | Better |
|---|---|---|
| Correctness | Better | Better |
A missing user is None, not a panic. | ||
| Failure behavior | Better | Better |
The caller must handle None. The compiler checks it. | ||
| Performance | Same | Better |
| "OK" still copies the user. "Better" copies nothing. | ||
| Readability | Better | Better |
| One line instead of seven. | ||
| API ergonomics | Better | Better |
A slice parameter accepts a Vec, an array, or part of a list. | ||
Trade-offs
Section titled “Trade-offs”- “OK” is correct when the caller needs to own the user, for example to store it after the list is changed. Even then, it is often better to return
Option<&User>and let that one caller write.cloned(). - A linear search is fine for a short list. For many lookups in a long list, a
HashMap<u64, User>is the better design, andmap.get(&id)already returnsOption<&User>. - A panic can be correct if the ID comes from your own code and a missing user is a bug. Then use
expectwith a message that says why the user must exist, and keep the function name honest, for exampleuser_by_known_id.