Skip to content

← Errors Are Values step 5 of 24

Medium Primitives

Borrowing through Option

Summarise two optional fields without consuming either of them.

pub fn summarize(
    name: Option<String>,
    tags: Option<Vec<String>>,
) -> (usize, Option<String>, Vec<usize>)

Return three things:

  1. the length of name in bytes, or 0 if there is no name;
  2. name itself, handed straight back — same Option, same String;
  3. the byte length of each tag, or an empty vector if there are no tags.

Requirement 2 is the whole exercise. You cannot compute the length by consuming the name, because you have to give it back afterwards.

Read E0507 first

The starter does not compile:

error[E0507]: cannot move out of `*s` which is behind a shared reference
   |
   |     let owned: Option<String> = name.as_ref().map(|s| *s);
   |                                                       ^^
   |                                    move occurs because `*s` has type
   |                                    `String`, which does not implement
   |                                    the `Copy` trait

This is the most demoralising family of beginner error in Rust, because the code reads correctly. Here is what actually happened, in order:

  • name is an Option<String> you own.
  • name.as_ref() turns &Option<String> into Option<&String> — a new Option, borrowing the same String. Nothing moved. This is the whole point of as_ref and it is the single most useful method on Option.
  • .map(|s| *s) then hands your closure an &String and you write *s, which says “give me the String itself”. You cannot take a String out of a borrow: whoever you borrowed it from still expects it to be there.

The related errors you will meet on the way out: E0382 (“use of moved value”) if you consume name with map and then try to return it, E0596 (“cannot borrow as mutable”) if you reach for as_mut without a mut binding, and E0308 whenever &String and String get confused.

The five methods, and the one distinction that matters

let name: Option<String> = Some("ada".to_string());

name.as_ref()     // Option<&String>   — borrow the contents
name.as_mut()     // Option<&mut String> (needs `let mut name`)
name.as_deref()   // Option<&str>      — borrow, then deref-coerce

and going the other way, from a borrowed Option back to an owned one:

let borrowed: Option<&String> = name.as_ref();
borrowed.cloned()   // Option<String>   — needs T: Clone
let n: Option<&i32> = Some(&1);
n.copied()          // Option<i32>      — needs T: Copy, no allocation

as_deref is as_ref followed by a deref: Option<String> becomes Option<&str>, Option<Vec<T>> becomes Option<&[T]>. It only works where T: Deref, so those two plus Box<T> and friends — not for an arbitrary struct. It is worth reaching for because &str and &[T] are the types the rest of the standard library actually wants.

Once you have Option<&str>, this whole problem is two lines:

let len = name.as_deref().map_or(0, str::len);

and for the tags, as_deref() gives you Option<&[String]>, whose unwrap_or_default() is the empty slice — no allocation, no if.

Cloning is not the answer here

The obvious escape is name.clone(), compute on the copy, return the original. It compiles. It also allocates a whole string to read one integer off it, and #![deny(clippy::redundant_clone)] at the top of the file will reject it:

error: redundant clone

.clone() is not forbidden in Rust and never should be — but it should be a decision, not a reflex you reach for when the borrow checker says no. Almost every “I had to clone it” in a beginner’s code is an as_ref that was never learned.

An API rule worth adopting today

Take Option<&T>, not &Option<T>:

fn bad(name: &Option<String>) -> usize { … }   // clippy::ref_option
fn good(name: Option<&str>) -> usize { … }

&Option<T> forces the caller to have an Option in hand. Option<&T> is strictly more general — the caller can produce one from an Option<T> with .as_deref(), from an &Option<T> with .as_deref(), or out of thin air with Some(&x). And a caller who only has a &T cannot invent an &Option<T> at all without allocating one.

Notes

  • str::len counts bytes, not characters. "héllo" is 6 bytes and 5 chars. The tests check this; use chars().count() when you want characters.
  • map_or(default, f) is map(f).unwrap_or(default) in one call. Its argument order surprises everyone once: the default comes first.
  • useless_asref is the lint for the opposite mistake — calling as_ref() where the value is already a reference.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…