12. Return a collection
The task
Section titled “The task”A user store holds a list of users. Write a method that returns the active ones.
The versions
Section titled “The versions”WorseReturns
Vec<User>: a full copy of each active user, with all their strings.pub struct UserStore {pub users: Vec<User>,}impl UserStore {/// A copy of every active user, and of all their strings.pub fn active_users(&self) -> Vec<User> {let mut result = Vec::new();for user in &self.users {if user.active {result.push(user.clone());}}result}}OKReturns
Vec<&User>: references into the store. One smallVec, no copies of users.pub struct UserStore {pub users: Vec<User>,}impl UserStore {/// References to the active users. One small allocation, no string copies.pub fn active_users(&self) -> Vec<&User> {self.users.iter().filter(|u| u.active).collect()}}BetterReturns
impl Iterator<Item = &User>: a lazy view. And&[User]for all users, because that data already exists as a slice.pub struct UserStore {users: Vec<User>,}impl UserStore {pub fn new(users: Vec<User>) -> Self {Self { users }}/// A lazy view of the active users. The caller decides what to collect, if anything.pub fn active_users(&self) -> impl Iterator<Item = &User> {self.users.iter().filter(|u| u.active)}/// All users, as a slice: the data already exists in this form.pub fn all_users(&self) -> &[User] {&self.users}}
The “Worse” version is the Java defensive copy: return a new list so that the caller cannot change the store. In Rust, a shared reference &User already cannot change the user, so the copy protects nothing. It costs one heap allocation per String field of each active user. The test worse_copies_the_strings shows that each name is in a new heap buffer. See S01: clone() to satisfy the borrow checker.
The “OK” version returns references. The borrow checker makes sure that the store lives as long as the Vec<&User>, and that nobody changes the store while the references exist. It still allocates one Vec, which the caller may not need.
The “Better” version returns an iterator. The caller decides what to do with it:
pub fn callers(store: &UserStore) -> (usize, Option<&str>, Vec<String>) { let count = store.active_users().count(); // no `Vec` at all let first = store.active_users().next().map(|u| u.name.as_str()); // stops at the first let emails = store.active_users().map(|u| u.email.clone()).collect(); // the caller collects (count, first, emails)}count()needs noVecat all.next()stops at the first active user.map(...).collect()builds exactly the collection the caller needs, in one pass. Collecting aVec<&User>first and then mapping it would be S11:collect()then iterate again.
all_users returns &[User]. When the data already exists in the right shape, a slice is the simplest return type: no allocation, and the caller gets every slice method (len, iter, first, binary_search).
Verdict
Section titled “Verdict”| Criterion | OK | Better |
|---|---|---|
| Correctness | Same | Same |
| All three return the same users. | ||
| Failure behavior | Same | Same |
| Nothing can fail. | ||
| Performance | Better | Better |
"Worse" copies each user and its strings. "OK" allocates one Vec of pointers. "Better" allocates nothing until the caller collects. | ||
| Readability | Better | Better |
One filter line instead of a loop with push. | ||
| API ergonomics | Better | Same |
"OK" gives a Vec that the caller can index and iterate many times. "Better" must be called again for a second pass, and its type cannot be named in a struct field. | ||
Trade-offs
Section titled “Trade-offs”- Return
Vec<User>when the caller must own the data: to send it to another thread, to keep it after the store is gone, or to change it. Then the copy has a reason. Name it so (active_users_owned, orinto_active_users(self)). impl Iteratorborrows the store. While the caller holds the iterator, the store cannot be changed. Usually that is fine. For a long-lived result, collect it.impl Iteratoris hard to put in a trait or a struct field, because its type has no name. Inside a trait, Rust supportsimpl Traitin return position since 1.75. For a struct field, collect into aVec, or useBox<dyn Iterator>.- A slice is right only when the data already has that shape.
&[User]cannot be built from a filter without allocating.