Skip to content

← Ownership II: Borrowing and the Borrow Checker step 11 of 24

Medium Primitives

ref, ref mut, and default binding modes in edition 2024

Update every present value in a mutable slice of options, and count both present and empty slots.

pub fn bump_present(opts: &mut [Option<i64>], by: i64) -> (usize, usize)

Add by to every Some(value) in place. Leave every None unchanged. Return (present_count, empty_count). A present value counts even when it is zero or by == 0; the counts always add up to the slice length.

The grader represents None as JSON null and present values as signed 64-bit integers. Inputs guarantee that each addition fits in i64.

[],          by=5   -> (0, 0), []
[null,null], by=5   -> (0, 2), [null,null]
[1,2,3],     by=10  -> (3, 0), [11,12,13]
[1,null,-4], by=-3  -> (2, 1), [-2,null,-7]
[0,null],    by=0   -> (1, 1), [0,null]

The starter uses iter(), which only gives it shared references. Choose an iterator that permits updating the values through the slice's existing mutable borrow.

Read the type being matched

In the pattern Some(v), the type of v depends on the value being matched:

Matched value's type Binding type in Some(v)
Option<i64> i64
&Option<i64> &i64
&mut Option<i64> &mut i64

This behavior is called match ergonomics. When a non-reference pattern matches through a reference, the compiler can follow the reference and adjust how it binds the contents. In the last two rows, it borrows the inner integer instead of taking an owned integer out.

The following complete example uses the mutable-reference case:

fn main() {
    let mut item = Some(4_i64);
    let slot: &mut Option<i64> = &mut item;
    if let Some(value) = slot {
        let value: &mut i64 = value;
        *value += 3;
    }
    assert_eq!(item, Some(7));
}

The extra type annotation is only there to show what the compiler inferred. Dereferencing value with *value accesses the borrowed integer.

Why the starter cannot mutate its values

opts.iter() yields &Option<i64>, so Some(v) binds v as &i64. The assignment *v += by fails with E0594 because a shared reference does not permit that write.

opts.iter_mut() instead yields &mut Option<i64>. The same pattern now binds v as &mut i64, making the assignment valid. The function's input already provides a mutable slice, so this repair changes how the loop borrows its elements rather than requiring a different function signature.

Keep the empty branch too: this task asks for both counts. Flattening the options can be useful when only present values matter, but would omit the empty slots from that traversal.

Explicit reference bindings

ref and ref mut are pattern modifiers that request a borrowed binding. An &mut pattern, by contrast, matches a mutable reference. They serve different roles.

This complete example spells out both the outer reference pattern and the inner mutable-reference binding:

fn main() {
    let mut item = Some(4_i64);
    let slot = &mut item;
    if let &mut Some(ref mut value) = slot {
        *value += 3;
    }
    assert_eq!(item, Some(7));
}

It is valid Rust 2024, though the shorter ergonomic form is easier to read for this task. An explicit pattern is useful when explaining which reference is being matched and which binding is being borrowed.

Rust 2024 restricts mixed implicit and explicit patterns

The default binding mode starts as move. Matching through references can change it to ref or ref mut; nested shared references can constrain it further. The three-row table above describes the simple Option<i64> case, not every possible nested pattern.

In Rust 2024, explicit mut, ref, or ref mut binding modifiers require the default mode at that point to be move. Reference patterns such as &x also need an explicit path through the outer references.

For example, this is a complete program that compiles in edition 2021 but is rejected in edition 2024:

fn main() {
    let [ref value] = &[7_i64];
    assert_eq!(*value, 7);
}

The outer reference was handled implicitly, so the binding already uses reference mode. Remove the redundant ref to make this version valid in both editions:

fn main() {
    let [value] = &[7_i64];
    assert_eq!(*value, 7);
}

This does not mean that ref adds another reference layer whenever a binding is already borrowed. The older pattern above also binds value as &i64. Rust 2024 rejects the redundant spelling rather than assigning it a new type.

The edition guide covers the corresponding restrictions for mut and reference patterns, with migration examples. The compiler's rust_2024_incompatible_pat lint helps identify these cases when migrating an older-edition crate. This site's Rust submissions already compile as edition 2024.

For more involved patterns, trace the matched type and reference layers using the binding-mode rules. A mutable variable binding and a mutable reference are different: writing mut v cannot turn an &i64 into permission to change the integer.