Skip to content

← Errors Are Values step 5 of 24

Medium Primitives

Borrowing through Option

Read two optional fields, return the name unchanged, and collect byte lengths:

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

The function takes ownership of both arguments. While calculating lengths, borrow their contents so that you can return the original name without cloning it. Return the tuple in this order:

  1. The name's byte length, or 0 for None.
  2. The original name, preserving None versus Some("").
  3. Each tag's byte length in input order. Both None and an empty tag vector give an empty length vector.

The tags are not returned; they are dropped when the function finishes. Keep duplicates and empty tags in the output rather than filtering them.

Name Tags Result
Some("ada") Some(["rust", "errors"]) (3, Some("ada"), [4, 6])
None Some(["a"]) (0, None, [1])
Some("") None (0, Some(""), [])
Some("héllo") Some(["ü", ""]) (6, Some("héllo"), [2, 0])

The table omits the String and Vec constructors for readability. String::len counts UTF-8 bytes. chars().count() counts Unicode scalar values, which is a different measure and is not always the number of characters a reader perceives.

Why the starter fails

name.as_ref() borrows the option's contents, producing Option<&String>. The starter then calls .map(|s| *s). Its closure receives a shared reference and tries to move the String out through it. Rust rejects that move with E0507 because the borrowed value must remain available to its owner.

Compute the length through the reference instead. map consumes the option it is called on, so use an option containing references when the original owned option must remain available. Calling name.map(...) directly would move name and prevent returning it later in this implementation.

Choose the kind of access you need

For name: Option<String>, these methods have different results:

Expression Result Purpose
name.as_ref() Option<&String> Read the contained string through a shared reference
name.as_mut() Option<&mut String> Modify it through a mutable reference; requires mutable access to name
name.as_deref() Option<&str> Borrow through String's Deref implementation
name.as_ref().cloned() Option<String> Clone the contained string when present

For a copyable value, Option<&T>::copied() produces Option<T>. The Option documentation describes the bounds and variants of these methods.

This complete example separates borrowing, mutation, cloning and copying:

fn main() {
    let mut name = Some(String::from("ada"));
    assert_eq!(name.as_deref(), Some("ada"));

    let snapshot = name.as_ref().cloned();
    if let Some(text) = name.as_mut() {
        text.push('!');
    }
    assert_eq!(name.as_deref(), Some("ada!"));
    assert_eq!(snapshot.as_deref(), Some("ada"));

    let number = 7;
    let borrowed_number = Some(&number);
    assert_eq!(borrowed_number.copied(), Some(7));
}

The snapshot needs independent ownership because the original is modified afterwards. Reading a string's length does not need such a snapshot.

Apply borrowing to this task

Use as_deref() to view the name as Option<&str>. Then map_or(0, str::len) supplies zero for None and computes the length for Some. The default argument comes first.

For the tags, as_deref() produces Option<&[String]>. unwrap_or_default() gives an empty slice when the option is absent. Iterate over that slice and collect each length into the result vector. Obtaining the borrowed slice does not clone the tags; collecting lengths creates the separate output vector.

Keep #![deny(clippy::redundant_clone)] from the starter. It rejects the redundant-clone patterns that Clippy detects, not every possible clone or allocation. Passing value tests or a lint check alone does not prove that an implementation preserved the original allocation. Follow the task's borrowing requirement directly.

A useful helper signature

A helper that only needs optional text can accept Option<&str>:

fn name_len(name: Option<&str>) -> usize {
    name.map_or(0, str::len)
}

fn main() {
    let owned = Some(String::from("ada"));
    assert_eq!(name_len(owned.as_deref()), 3);
    assert_eq!(name_len(Some("grace")), 5);
    assert_eq!(name_len(None), 0);
}

This lets a caller pass text without first building an Option<String>. It is an API choice, not a rule that &Option<T> is always wrong. A helper may need access to the container itself. Constructing an Option also does not inherently require heap allocation; the contained value determines what storage it needs.