Skip to content

4. Process a list of orders

Turn a list of orders into a list of invoices, with the order ID, the customer, and the total.

  1. WorseClones the whole list, then clones the lines and the customer of each order again.

    pub fn make_invoices(orders: &Vec<Order>) -> Vec<Invoice> {
    let mut invoices = Vec::new();
    for order in orders.clone() {
    let lines = order.lines.clone();
    let total_cents = lines.iter().sum();
    invoices.push(Invoice {
    order_id: order.id,
    customer: order.customer.clone(),
    total_cents,
    });
    }
    invoices
    }
  2. OKBorrows the orders. Clones only the customer name, because the invoice owns it.

    pub fn make_invoices(orders: &[Order]) -> Vec<Invoice> {
    orders
    .iter()
    .map(|order| Invoice {
    order_id: order.id,
    customer: order.customer.clone(), // the invoice owns its own copy
    total_cents: order.lines.iter().sum(),
    })
    .collect()
    }
  3. BetterTakes the orders by value and moves each customer name into its invoice. No copies at all.

    pub fn into_invoices(orders: Vec<Order>) -> Vec<Invoice> {
    orders
    .into_iter()
    .map(|order| Invoice {
    order_id: order.id,
    total_cents: order.lines.iter().sum(),
    customer: order.customer, // moved, not copied
    })
    .collect()
    }

The “Worse” version has three copies for each order: orders.clone() copies every order, order.lines.clone() copies the lines again, and order.customer.clone() copies the name again. Copies like these usually come from borrow errors during development. The agent tried to move order.customer out of a borrowed order, got “cannot move out of order.customer, which is behind a shared reference” (E0507), and added .clone() calls until the errors stopped. See S01: clone() to satisfy the borrow checker.

The difference between “OK” and “Better” is a question of ownership, not of style:

  • “OK” borrows (&[Order]). The caller keeps its orders. The invoices need their own customer names, so the function clones one String for each invoice. That clone has a reason.
  • “Better” consumes (Vec<Order>). The caller gives the orders away. into_iter() gives each order by value, so order.customer can move into the invoice. The test better_reuses_the_customer_strings proves that the invoice uses the same heap buffer as the order.
Each version compared with the Worse version
CriterionOKBetter
Correctness Same Same
All three give the same invoices.
Failure behavior Same Same
Nothing can fail.
Performance Better Better
"Worse" copies every order twice. "OK" copies one string for each order. "Better" copies nothing.
Readability Better Better
An iterator chain with one map. Each remaining clone has a reason.
API ergonomics Better Same
"OK" accepts any slice. "Better" needs an owned Vec, so it fits only callers that are finished with their orders.
  • “OK” is the right default. Most callers still need their orders after they make the invoices: to save them, to log them, or to show them. A function that borrows works for every caller.
  • “Better” is right at the end of a pipeline, where the orders are not needed again: for example, the last step of an import job. Name it so that the move is clear (into_invoices), as the standard library does with into_iter and into_bytes.
  • Offer both if callers need both. into_invoices(orders) and make_invoices(&orders) next to each other is a common pattern.
  • “Worse” is never better than “OK”. The copies do nothing that borrowing does not do.