We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership II: Borrowing and the Borrow Checker step 7 of 24
E0499: cannot borrow as mutable more than once
pub fn apply_swaps(xs: &mut [i64], swaps: &[[usize; 2]]) -> usize
Apply each [i, j] pair in order, exchanging xs[i] and xs[j]. If either
index is out of range, skip that pair. Return how many pairs were skipped.
[1,2,3], [[0,2]] -> [3,2,1], skipped 0
[1,2,3], [[1,1]] -> [1,2,3], skipped 0 <- i == j is a NO-OP, not a skip
[1,2,3], [[0,5]] -> [1,2,3], skipped 1
[], [[0,0]] -> [], skipped 1
Read the second line again, because it is the interesting one and there is a
test for it. A pair like [1, 1] is in range. Swapping an element with
itself changes nothing and is not an error.
The starter, and why it cannot work
let a = &mut xs[i];
let b = &mut xs[j]; // error[E0499]: cannot borrow `xs[_]` as mutable
std::mem::swap(a, b); // more than once at a time
You know from item 3.3 why: xs[i] goes through IndexMut, which takes
&mut xs — the whole slice — and hands back a reference derived from it. Two
such calls are two overlapping exclusive loans of the same place. The index
values are runtime data; the compiler has no theory of arithmetic that would
let it conclude i != j, and in this program it cannot, because sometimes
i == j really is true.
This is the first item where the answer is not “restructure”
Up to now, every fix has been “rearrange your own code”: copy the value out,
end the loan earlier, compute then mutate. That does not work here. There is
no arrangement of &mut xs[i] and &mut xs[j] that the checker will accept,
because the thing you want it to believe is genuinely not derivable from the
types in front of it.
So the move is different, and it is the move the entire expert half of Rust is built on:
When the checker cannot construct the proof, find the API whose signature already contains it.
Somebody has done this work. They wrote a function whose type says “these references are disjoint”, discharged the obligation once inside it, and published it. You do not need to be clever; you need to know the catalogue.
get_disjoint_mut
fn get_disjoint_mut<const N: usize>(&mut self, indices: [usize; N])
-> Result<[&mut T; N], GetDisjointMutError>
Stabilised in Rust 1.86 (it spent its unstable life called get_many_mut).
You hand it an array of indices; it hands you back an array of exclusive
references, or an error. That single Result is the whole lesson: the runtime
check is the proof obligation the compiler could not discharge. It did not
vanish; somebody moved it from compile time to run time and made you handle
the failure.
And look at the error type. It has exactly two variants:
-
IndexOutOfBounds -
OverlappingIndices
Those are precisely the two things the compiler could not rule out. The API is a specification of what the borrow checker was worried about. Rust is full of this shape once you start looking.
For this problem, OverlappingIndices is the i == j case — so Err here
means “nothing to do”, not “something went wrong”. Do not count it as a skip.
There is also a get_disjoint_unchecked_mut. It is unsafe, it skips the
check, and it is not to be used here — #![forbid(unsafe_code)] is the
standing rule from this track through Track 16, and “I was sure the indices
differed” is exactly the reasoning that makes C memory-unsafe.
The one-liner you should also know
Slices have had swap since forever:
xs.swap(i, j);
It takes two indices, panics if either is out of range, and explicitly allows
i == j. It is the right answer for this specific job, and if you write the
three-line temporary-variable swap by hand, clippy will fire manual_swap at
you. (Get the order of the three lines wrong and you get almost_swapped as
well — a double punishment for a very old bug.)
So why introduce get_disjoint_mut at all? Because slice::swap is a
special case — it works because it is one function that owns both accesses.
The moment you need to do something to two elements that is not exchange them,
swap is no help and get_disjoint_mut is. Learn the general tool on the
problem where the specific tool also happens to exist.
Whichever route you take, be careful with for i in 0..swaps.len() — clippy’s
needless_range_loop will send you to for s in swaps.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.