Skip to content

Rust Standard Library Idioms: Small Tricks You'll Use Every Day

Every kitchen has a drawer of small tools: a peeler, a bottle opener, a garlic press. You could do every job with one big knife, but the small tools make the job quicker and safer. Rust’s standard library has a drawer like that too.

This page is a tour of that drawer. Each trick is short: a real-life picture, a runnable example, and a “use it when…” line so you know when to reach for it. You don’t need to learn them all at once. Skim the page now, and come back when you see a pattern in your own code that feels clumsy.

Moving a value out of &mut: mem::take, replace, swap

Section titled “Moving a value out of &mut: mem::take, replace, swap”

Picture a rubbish bin with a bag in it. You may take the full bag out, but only if you put a fresh bag in right away. The bin is never left without a bag.

Rust has the same rule. If you only have &mut self, you can’t simply move a field out, because that would leave a hole in the struct. The compiler stops you with E0507. The std::mem functions solve this by always leaving something behind:

  • mem::take(&mut x) takes the value and leaves the type’s default (an empty Vec, an empty String, 0, None).
  • mem::replace(&mut x, new) puts new in and gives you the old value.
  • Option::take() takes what’s inside an Option and leaves None.
use std::mem;
struct Inbox {
unread: Vec<String>,
status: String,
draft: Option<String>,
}
impl Inbox {
// Hand over every unread message and leave an empty Vec behind.
fn read_all(&mut self) -> Vec<String> {
mem::take(&mut self.unread)
}
// Put a new status in and get the old one back.
fn set_status(&mut self, new: &str) -> String {
mem::replace(&mut self.status, new.to_string())
}
// Take the draft out (if there is one) and leave None.
fn send_draft(&mut self) -> Option<String> {
self.draft.take()
}
}
fn main() {
let mut inbox = Inbox {
unread: vec!["Lunch?".to_string(), "Invoice #42".to_string()],
status: "online".to_string(),
draft: Some("See you at 6".to_string()),
};
let messages = inbox.read_all();
println!("read {:?}, still unread: {:?}", messages, inbox.unread);
let old = inbox.set_status("in a meeting");
println!("status was '{old}', now '{}'", inbox.status);
println!("sent: {:?}", inbox.send_draft());
println!("sent again: {:?}", inbox.send_draft());
}

Output:

read ["Lunch?", "Invoice #42"], still unread: []
status was 'online', now 'in a meeting'
sent: Some("See you at 6")
sent again: None
▶ Try it Yourself

No clone() anywhere: the messages and the old status are moved out, not copied. That’s why these functions are both safe and cheap.

Two relatives finish the set. mem::swap(&mut a, &mut b) trades two values, like swapping two plates on a table. Option::replace(new) puts a new value into an Option and gives back the old one.

use std::mem;
fn main() {
// Fill one list while the other one is being worked on.
let mut filling = vec!["order 1", "order 2"];
let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing);
println!("processing {processing:?}, filling {filling:?}");
// Option::replace: put a new value in, get the old one back.
let mut now_playing = Some("Song A");
let previous = now_playing.replace("Song B");
println!("was {previous:?}, now {now_playing:?}");
}

Output:

processing ["order 1", "order 2"], filling []
was Some("Song A"), now Some("Song B")
▶ Try it Yourself

Use it when you only have &mut access but need to own the value inside.

Think of windows(2) as a magnifying glass sliding along a ruler: it shows two marks at a time, then moves one step. chunks(3) is like packing eggs into boxes of three: the last box may be only partly full. chunks_exact(3) only gives you full boxes and keeps the leftover eggs aside in .remainder().

These methods work on any slice, so they work on arrays and vectors too.

fn main() {
let temps = [12, 15, 14, 19, 21];
// windows(2): every pair of neighbours, overlapping
for pair in temps.windows(2) {
println!("{} -> {}: change {}", pair[0], pair[1], pair[1] - pair[0]);
}
// chunks(3): batches of 3, the last one may be smaller
let emails = ["a@x.com", "b@x.com", "c@x.com", "d@x.com", "e@x.com"];
for batch in emails.chunks(3) {
println!("sending to {batch:?}");
}
// chunks_exact(2): only full batches; the rest waits in remainder()
let seats = [1, 2, 3, 4, 5];
let pairs = seats.chunks_exact(2);
let leftover = pairs.remainder();
for pair in pairs {
println!("pair {pair:?}");
}
println!("leftover: {leftover:?}");
}

Output:

12 -> 15: change 3
15 -> 14: change -1
14 -> 19: change 5
19 -> 21: change 2
sending to ["a@x.com", "b@x.com", "c@x.com"]
sending to ["d@x.com", "e@x.com"]
pair [1, 2]
pair [3, 4]
leftover: [5]
▶ Try it Yourself

A few more slice helpers answer common questions without a loop. split_first() gives you the first item and the rest, like a teacher reading the first name on a list and handing the rest of the list to a helper. split_last() does the same from the end.

fn main() {
let command = ["git", "commit", "-m", "fix typo"];
// split_first: the first item, and a slice of the rest
if let Some((program, args)) = command.split_first() {
println!("program: {program}, args: {args:?}");
}
// split_last: the last item, and everything before it
if let Some((message, _rest)) = command.split_last() {
println!("message: {message}");
}
println!("is git? {}", command.starts_with(&["git"]));
println!("has -m? {}", command.contains(&"-m"));
// iter().rev(): walk backwards without changing the slice
let history = ["open", "edit", "save"];
let undo_order: Vec<_> = history.iter().rev().collect();
println!("undo order: {undo_order:?}");
}

Output:

program: git, args: ["commit", "-m", "fix typo"]
message: fix typo
is git? true
has -m? true
undo order: ["save", "edit", "open"]
▶ Try it Yourself

split_first and split_last return an Option, because an empty slice has no first item. That’s why we use if let.

Use it when you need neighbours (windows), batches (chunks), or “the head and the rest” (split_first).

Iterator helpers you’ll use all the time

Section titled “Iterator helpers you’ll use all the time”

The Iterators page covers map, filter, sum and collect. Here are the next tools in the box.

Imagine dealing cards. Sometimes you want to look at the next card before you decide to draw it. .peekable() gives an iterator that can do that: .peek() shows the next item without taking it, and .next_if(...) takes it only if it passes a test. This is exactly what small parsers need.

fn main() {
let input = "12+345-6";
let mut chars = input.chars().peekable();
let mut tokens: Vec<String> = Vec::new();
while let Some(c) = chars.next() {
if c.is_ascii_digit() {
let mut number = c.to_string();
// Take the next char only while it is a digit too.
while let Some(d) = chars.next_if(|next| next.is_ascii_digit()) {
number.push(d);
}
tokens.push(number);
} else {
tokens.push(c.to_string());
}
}
println!("{tokens:?}");
}

Output:

["12", "+", "345", "-", "6"]
▶ Try it Yourself

Without peekable, you would read the + while looking for more digits, and then it would be gone.

Use it when the next item decides what you do with the current one.

Walking lists: zip, enumerate, step_by, take_while, skip_while, chain

Section titled “Walking lists: zip, enumerate, step_by, take_while, skip_while, chain”
Method Think of it as Use it when…
zip a zipper joining two sides tooth by tooth you have two lists that belong together
enumerate numbering the lines of a list you need the index as well as the item
step_by(n) climbing stairs two (or n) at a time you want every n-th item
take_while eating sweets until you hit one you don’t like you want the start of a list, up to the first “no”
skip_while skipping adverts until the show starts you want the rest, from the first “no” on
chain coupling two train carriages you want one list after another
fn main() {
let names = ["Ana", "Ben", "Chen"];
let scores = [88, 92, 75];
// zip pairs two lists up; enumerate adds a counter starting at 0
for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() {
println!("{}. {name}: {score}", i + 1);
}
// step_by: every 15th minute of the hour
let quarters: Vec<u32> = (0..60).step_by(15).collect();
println!("quarter hours: {quarters:?}");
// into_iter() on an array gives the numbers themselves
let queue = [3, 5, 8, 12, 4];
let small: Vec<i32> = queue.into_iter().take_while(|n| *n < 10).collect();
let rest: Vec<i32> = queue.into_iter().skip_while(|n| *n < 10).collect();
println!("before the first big one: {small:?}, from there on: {rest:?}");
// chain: one list after the other
let morning = ["email", "standup"];
let afternoon = ["review", "deploy"];
let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect();
println!("{day:?}");
}

Output:

1. Ana: 88
2. Ben: 92
3. Chen: 75
quarter hours: [0, 15, 30, 45]
before the first big one: [3, 5, 8], from there on: [12, 4]
["email", "standup", "review", "deploy"]
▶ Try it Yourself

Notice that take_while stops at 12 for good. The 4 after it is small, but it never gets checked. That’s the difference from filter, which looks at every item.

Answering questions: fold, position, find_map, partition, flat_map

Section titled “Answering questions: fold, position, find_map, partition, flat_map”
Method Think of it as Use it when…
sum a calculator’s plus key you just want the total
fold a snowball rolling downhill, growing at each step you build one result your own way
position “which seat number is Ben in?” you need the index of the first match
find_map trying keys until one opens the door you try something on each item and keep the first success
partition sorting laundry into two baskets you split one list into two by a yes/no question
flat_map emptying several bags onto one table each item turns into several items
fn main() {
let orders = [12, 40, 7, 55, 30];
// sum: just add them up
let total: i32 = orders.iter().sum();
// fold: add them up your way (here: each order also pays a 2 euro fee)
let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2);
println!("total {total}, with fees {with_fees}");
// position: the index of the first match
println!("first order over 50 is at index {:?}", orders.iter().position(|p| *p > 50));
// partition: two Vecs, one for "yes" and one for "no"
let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30);
println!("big {big:?}, small {small:?}");
// find_map: try to parse each one, keep the first that works
let inputs = ["abc", "", "42", "7"];
let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok());
println!("first number: {first_number:?}");
// flat_map: every line becomes several words, all in one list
let lines = ["buy milk", "call mum now"];
let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect();
println!("{words:?}");
}

Output:

total 144, with fees 154
first order over 50 is at index Some(3)
big [40, 55, 30], small [12, 7]
first number: Some(42)
["buy", "milk", "call", "mum", "now"]
▶ Try it Yourself

fold takes a starting value (0) and a closure that gets “the result so far” and the next item. If all you need is a plain total, sum says it more clearly. Reach for fold when the rule is your own.

Cleaning up a Vec in place: dedup and retain

Section titled “Cleaning up a Vec in place: dedup and retain”

These two are Vec methods, not iterator methods, but they belong in the same drawer. dedup is like a DJ who never plays the same song twice in a row (the song can come back later). retain is a bouncer: only items that pass the test stay inside.

fn main() {
// dedup removes repeats that sit next to each other
let mut clicks = vec!["home", "home", "shop", "shop", "shop", "cart", "home"];
clicks.dedup();
println!("{clicks:?}");
// retain keeps only the items that pass the test
let mut cart = vec![("apple", 3), ("bread", 0), ("milk", 1), ("eggs", 0)];
cart.retain(|(_, qty)| *qty > 0);
println!("{cart:?}");
}

Output:

["home", "shop", "cart", "home"]
[("apple", 3), ("milk", 1)]
▶ Try it Yourself

The last "home" survived because it wasn’t next to another "home". To remove every duplicate, sort first, as the Sorting page shows.

Use it when you want to change a Vec without building a new one.

An Option is like a parcel that may or may not have arrived. You could open every parcel with a full match, but most of the time a one-word helper says the same thing. The Option page covers unwrap_or and match; here are the next helpers.

fn main() {
let nickname: Option<String> = None;
let age: Option<u32> = Some(17);
let width: Option<u32> = Some(3);
let height: Option<u32> = Some(4);
// unwrap_or_default: the value, or the type's default ("", 0, empty Vec...)
println!("nickname: '{}'", nickname.unwrap_or_default());
// map_or: a fallback if None, otherwise transform the value
println!("{}", age.map_or("age unknown".to_string(), |a| format!("{a} years old")));
// filter: keep the value only if it passes a test
println!("adult age: {:?}", age.filter(|a| *a >= 18));
// zip: Some only if BOTH are Some
println!("area: {:?}", width.zip(height).map(|(w, h)| w * h));
// and_then: chain a step that can itself give None
let good = Some("42").and_then(|s| s.parse::<u32>().ok());
let bad = Some("forty").and_then(|s| s.parse::<u32>().ok());
println!("parsed: {good:?} and {bad:?}");
}

Output:

nickname: ''
17 years old
adult age: None
area: Some(12)
parsed: Some(42) and None
▶ Try it Yourself

Inside a function, ? works on Option too: if the value is None, the function returns None right away. In a function that returns a Result, turn the Option into a Result first with ok_or(error) or ok_or_else(|| error). Use ok_or_else when building the error takes work, because it only runs when needed.

struct Employee {
name: String,
manager: Option<String>,
}
// `?` on an Option: if it's None, return None right away.
fn manager_initial(e: &Employee) -> Option<char> {
let manager = e.manager.as_ref()?;
manager.chars().next()
}
// ok_or_else turns None into an error, so `?` works in a Result function.
fn port_from(setting: Option<&str>) -> Result<u16, String> {
let raw = setting.ok_or_else(|| "PORT is not set".to_string())?;
raw.parse::<u16>().map_err(|e| e.to_string())
}
fn main() {
let ana = Employee { name: "Ana".to_string(), manager: Some("Grace".to_string()) };
let grace = Employee { name: "Grace".to_string(), manager: None };
println!("{}: {:?}", ana.name, manager_initial(&ana));
println!("{}: {:?}", grace.name, manager_initial(&grace));
println!("{:?}", port_from(Some("8080")));
println!("{:?}", port_from(None));
println!("{:?}", port_from(Some("eighty")));
// transpose: Option<Result<..>> becomes Result<Option<..>>
let retries_setting: Option<&str> = Some("3");
let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose();
println!("{retries:?}");
}

Output:

Ana: Some('G')
Grace: None
Ok(8080)
Err("PORT is not set")
Err("invalid digit found in string")
Ok(Some(3))
▶ Try it Yourself

transpose is rare, but it is the neat answer to “I have an optional setting, and parsing it can fail”. With it, you can put ? on the result and keep the Option.

Use it when a match on an Option has one interesting arm and one boring one.

Think of a coat check desk. You say a name, and the attendant either finds that hook or puts up a new one, then hands it to you. One trip, not two. That’s .entry(key). The HashMaps page shows entry(key).or_insert(value), the classic counter. Two more variations are worth knowing:

  • or_insert_with(f) only builds the default when the key is missing. or_default() does the same with the type’s default.
  • and_modify(f) changes the value if the key exists; chain or_insert after it for the “new key” case.
use std::collections::HashMap;
fn main() {
let sales = [("apples", 3), ("pears", 2), ("apples", 5), ("plums", 1), ("pears", 4)];
// and_modify + or_insert: update if present, insert if not
let mut totals: HashMap<&str, i32> = HashMap::new();
for (fruit, qty) in sales {
totals.entry(fruit).and_modify(|t| *t += qty).or_insert(qty);
}
// or_insert_with: create the empty Vec only when it's needed
let mut orders: HashMap<&str, Vec<i32>> = HashMap::new();
for (fruit, qty) in sales {
orders.entry(fruit).or_insert_with(Vec::new).push(qty);
}
// HashMap order is random, so sort the keys before printing
let mut fruits: Vec<&str> = totals.keys().copied().collect();
fruits.sort();
for fruit in fruits {
println!("{fruit}: total {}, orders {:?}", totals[fruit], orders[fruit]);
}
}

Output:

apples: total 8, orders [3, 5]
pears: total 6, orders [2, 4]
plums: total 1, orders [1]
▶ Try it Yourself

Use it when you would otherwise write “if the key exists, change it, else insert it”, which looks the key up twice.

The never type !: code that never comes back

Section titled “The never type !: code that never comes back”

Some roads are one-way: once you drive in, you never come back out. In Rust, an expression that never finishes has the never type, written !. panic!, return, break, continue, an endless loop and std::process::exit all have this type.

Because a ! value can never actually exist, the compiler lets it stand in for any type. That’s why a match can have one arm that gives an i32 and another that does return: the second arm never produces a value, so it can’t break the types.

use std::process;
// `-> !` means "this function never returns"
fn quit(message: &str) -> ! {
println!("{message}");
process::exit(0)
}
fn level_points(level: &str) -> u32 {
let points: u32 = match level {
"easy" => 10,
"hard" => 30,
_ => return 0, // `return` has type `!`, so it fits where a u32 is expected
};
points * 2
}
fn main() {
println!("easy: {}, hard: {}, weird: {}", level_points("easy"), level_points("hard"), level_points("weird"));
let mut total = 0;
for input in ["4", "oops", "6"] {
let n: i32 = match input.parse() {
Ok(n) => n,
Err(_) => continue, // `continue` is also `!`
};
total += n;
}
println!("total: {total}");
let config_file: Option<&str> = None;
let config = match config_file {
Some(path) => path,
None => quit("no config file found, stopping"),
};
println!("loaded {config}"); // never printed
}

Output:

easy: 20, hard: 60, weird: 0
total: 10
no config file found, stopping
▶ Try it Yourself

One warning: process::exit stops the program immediately, without running cleanup code (destructors) for values that are still alive. In real programs, returning from main is usually the gentler choice.

Two macros use ! as placeholders while you sketch code. todo!() means “I’ll write this later”. unimplemented!() means “this isn’t supported here”. Both panic if they are ever reached, but they let the rest of your program compile:

fn export_pdf(report: &str) -> Vec<u8> {
todo!("PDF export comes in v2")
}

If you call it, you get not yet implemented: PDF export comes in v2. With unimplemented!("PDF export") the message is not implemented: PDF export.

Use it when a function should stop the program, or a branch should leave early instead of giving a value.

These are the pocket tools: tiny, but you’ll use them every week.

  • matches!(value, pattern) answers “does this fit the pattern?” with true or false (more on the Pattern Matching page).
  • std::cmp::min / max pick the smaller or larger of two values, and .clamp(low, high) keeps a value inside a range, like a volume knob that stops at 100.
  • .abs_diff(other) gives the distance between two numbers. It’s always positive, and it never overflows, even for unsigned types.
  • dbg!(expr) prints the file, line, expression and value, then hands the value back, so you can wrap it around anything.
  • assert!(condition) panics if the condition is false. debug_assert! does the same, but only in debug builds, so it costs nothing in release builds.
use std::cmp::{max, min};
enum Order {
Pending,
Shipped,
Delivered,
Cancelled,
}
fn main() {
let orders = [Order::Pending, Order::Delivered, Order::Shipped, Order::Cancelled, Order::Pending];
// matches!: a yes/no pattern check, with | for "or"
let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count();
println!("open orders: {open}");
// min, max and clamp
println!("min {}, max {}", min(7, 3), max(7, 3));
let volume = 140;
let safe = volume.clamp(0, 100);
println!("volume {volume} clamped to {safe}");
// abs_diff: how far apart, never negative
let (guess, secret): (u32, u32) = (30, 42);
println!("you are {} away", guess.abs_diff(secret));
// dbg! prints to stderr and gives the value back
let doubled = dbg!(safe * 2);
// assert! stops the program if this is ever false
assert!(doubled <= 200, "doubled volume too high: {doubled}");
debug_assert!(safe <= 100); // only checked in debug builds
println!("all checks passed");
}

Output:

open orders: 3
min 3, max 7
volume 140 clamped to 100
you are 12 away
all checks passed
▶ Try it Yourself

The dbg! line isn’t in the output above, because dbg! writes to stderr, not stdout. In the Playground it shows up in the “Standard Error” panel:

[src/main.rs:28:19] safe * 2 = 200

That’s also why dbg! is safe to sprinkle around while debugging: it doesn’t mix into your program’s real output. Just remember to remove it before you ship.

Sorting has its own small tools. The Sorting page covers them in depth; here is the short version. sort_unstable is a little faster than sort and fine for plain numbers. sort_by_key sorts by one field. binary_search finds an item in sorted data very quickly, like opening a dictionary in the middle instead of reading from page one.

struct Player {
name: &'static str,
score: u32,
}
fn main() {
// sort_unstable: a bit faster; equal items may swap places
let mut ids = vec![42, 7, 19, 3, 25];
ids.sort_unstable();
println!("{ids:?}");
// binary_search: Ok(index) if found, Err(index) where it would go
println!("find 19: {:?}", ids.binary_search(&19));
println!("find 20: {:?}", ids.binary_search(&20));
// sort_by_key: sort by one field
let mut players = vec![
Player { name: "Ana", score: 120 },
Player { name: "Ben", score: 95 },
Player { name: "Chen", score: 150 },
];
players.sort_by_key(|p| p.score);
let names: Vec<&str> = players.iter().map(|p| p.name).collect();
println!("lowest to highest: {names:?}");
}

Output:

[3, 7, 19, 25, 42]
find 19: Ok(2)
find 20: Err(3)
lowest to highest: ["Ben", "Ana", "Chen"]
▶ Try it Yourself

Err(3) isn’t a failure to worry about. It tells you that 20 isn’t in the list, and that index 3 is where you could insert it to keep the list sorted.

Use it when you catch yourself writing a small if or loop that one of these already does.

Using mem::take on a type without Default. mem::take has to leave something behind, so it needs to know what “empty” means for the type:

use std::mem;
struct Connection {
host: String,
}
struct Pool {
active: Connection,
}
impl Pool {
fn reset(&mut self) -> Connection {
mem::take(&mut self.active) // ❌ what would an "empty" Connection be?
}
}
error[E0277]: the trait bound `Connection: Default` is not satisfied
--> src/main.rs:13:19
|
13 | mem::take(&mut self.active) // ❌ what would an "empty" Connection be?
| --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection`
| |
| required by a bound introduced by this call
|
help: consider annotating `Connection` with `#[derive(Default)]`

Fix it by choosing the replacement yourself with mem::replace(&mut self.active, Connection { host: "db2".to_string() }), or by storing the field as Option<Connection> and using self.active.take(). Only add #[derive(Default)] if an “empty” value really makes sense for the type.

Using ? on an Option in a function that returns Result. The two don’t mix automatically:

fn port_from(setting: Option<&str>) -> Result<u16, String> {
let raw = setting?; // ❌ None has no error message to return
raw.parse::<u16>().map_err(|e| e.to_string())
}
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
--> src/main.rs:2:22
|
1 | fn port_from(setting: Option<&str>) -> Result<u16, String> {
| ---------------------------------------------------------- this function returns a `Result`
2 | let raw = setting?; // ❌ None has no error message to return
| ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`

The compiler already tells you the fix: setting.ok_or("PORT is not set".to_string())?, or ok_or_else as in the example above. See E0277 for other forms of this error.

Expecting dedup or binary_search to work on unsorted data. dedup only removes repeats that are next to each other, and binary_search on unsorted data gives a meaningless answer. Neither one is a compiler error, so it’s easy to miss. Sort first.

Forgetting the leftovers of chunks_exact. A for loop over chunks_exact(3) silently skips the last 1 or 2 items. If they matter, handle .remainder(), or use chunks instead.

You have the prices [100, 104, 98, 110, 107]. Build a Vec with the change from each day to the next, print it, then print the biggest rise. Hint: windows(2) gives each pair as a small slice w, so the change is w[1] - w[0]. .iter().max() finds the biggest.

Show solution
fn main() {
let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect();
println!("changes: {changes:?}");
let biggest_rise = changes.iter().max();
println!("biggest rise: {biggest_rise:?}");
}

Output:

changes: [4, -6, 12, -3]
biggest rise: Some(12)
▶ Try it Yourself

Five prices give four changes, because windows(2) makes one pair per step. max() returns an Option, since an empty list has no biggest value.

Write a Printer struct with a queue: Vec<String> field. Give it a method add(&mut self, file: &str) and a method flush(&mut self) -> Vec<String> that hands back every queued file and leaves the queue empty. Add two files, flush, and print what was flushed and how many files are left. Hint: flush only has &mut self, so use std::mem::take.

Show solution
use std::mem;
struct Printer {
queue: Vec<String>,
}
impl Printer {
fn add(&mut self, file: &str) {
self.queue.push(file.to_string());
}
fn flush(&mut self) -> Vec<String> {
mem::take(&mut self.queue)
}
}
fn main() {
let mut printer = Printer { queue: Vec::new() };
printer.add("report.pdf");
printer.add("photo.png");
let jobs = printer.flush();
println!("printing {} jobs: {jobs:?}", jobs.len());
println!("left in queue: {}", printer.queue.len());
}

Output:

printing 2 jobs: ["report.pdf", "photo.png"]
left in queue: 0
▶ Try it Yourself

Writing self.queue on its own in flush would fail with E0507. mem::take moves the Vec out and leaves an empty one behind, with no copying.

Write a function first_port(candidates: &[&str]) -> Option<u16> that returns the first string that parses as a u16. Call it with ["", "abc", "70000", "8080", "9090"] and with ["none", "-1"], and print the port, or 3000 if there is none. Hint: find_map with s.parse::<u16>().ok(), then unwrap_or(3000).

Show solution
fn first_port(candidates: &[&str]) -> Option<u16> {
candidates.iter().find_map(|s| s.parse::<u16>().ok())
}
fn main() {
let from_config = ["", "abc", "70000", "8080", "9090"];
let all_bad = ["none", "-1"];
println!("using port {}", first_port(&from_config).unwrap_or(3000));
println!("using port {}", first_port(&all_bad).unwrap_or(3000));
}

Output:

using port 8080
using port 3000
▶ Try it Yourself

"70000" looks like a number, but it doesn’t fit in a u16 (the largest is 65535), so parsing fails and find_map moves on. It stops at "8080" and never even looks at "9090".

  • mem::take, mem::replace and Option::take move a value out of a &mut by leaving something in its place; mem::swap trades two values.
  • On slices, windows gives overlapping neighbours, chunks gives batches, and split_first / split_last give “one item and the rest”.
  • peekable lets you look ahead; zip, enumerate, chain, fold, find_map and partition replace many hand-written loops.
  • Option helpers like unwrap_or_default, map_or, filter and ok_or save you from writing small matches, and ? works on Option too.
  • entry() with or_insert_with or and_modify updates a HashMap in one lookup.
  • The never type ! belongs to code that never finishes (panic!, return, continue, process::exit), so it fits anywhere a value is expected.

Quick check

1. What does mem::take(&mut self.items) leave behind when items is a Vec?

2. What does [1, 2, 3, 4].windows(2) give you?

3. Why does let x: i32 = match s { "a" => 1, _ => return }; compile?

4. Your function returns a Result, but you have an Option. What do you add before ?

Score: 0 / 4