Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 10 of 20

Medium Primitives

Partial moves: taking one field and leaving the rest

Take structs apart field by field.

pub fn repack(records: Vec<(i64, String, Vec<String>)>) -> Vec<String>

Ownership is finer-grained than you think

So far “moved” has been a property of a variable. It isn’t. It is a property of a place — a variable, a field, a field of a field. The compiler tracks each independently, which is why this is legal:

let r = Record { id: 7, label: String::from("hi"), tags: vec![] };

let label = r.label;    // `r.label` has moved; `r.id` and `r.tags` have not
let id = r.id;          // fine — and would be fine even if `label` were a Vec

and why the error for over-reaching says something slightly different from the E0382s you have seen so far:

error[E0382]: use of partially moved value: `r`
  |
  |     let label = r.label;
  |                 ------- value partially moved here
  |     let r = Record { label: String::from("anon"), ..r };
  |                                                    ^ value used here after partial move

“Partially moved” is a real state. r is neither alive nor dead: some of it is still there, and the compiler knows exactly which parts. You may keep reading the fields that have not moved. You may not use r as a whole, because a whole Record is no longer what is sitting in that stack slot.

This is the same insight that later explains disjoint closure capture (a closure that touches r.label does not capture r.tags) and split borrows. It is also the most common real shape in ordinary code: “I just want the name out of this struct.”

The type

The grader injects this. Note what is not on it — no Clone, no Drop:

struct Record {
    id: i64,
    label: String,
    tags: Vec<String>,
}

The absence of Clone means there is no escape hatch: you cannot duplicate your way out of a partial move here. The absence of Drop is what makes the whole exercise possible at all, and the next problem is about what happens when a destructor shows up.

What to build

For each input tuple (id, label, tags):

  1. Build a Record from the three pieces.
  2. If the label is empty, replace it — using struct update syntax — with "anon", keeping the other fields: Record { label: String::from("anon"), ..r }.
  3. Move label out of the record. Move tags out. Read id after both of those moves, to prove the Copy field survived.
  4. Push format!("{id}:{label}[{}]", tags.join("+")).

(1, "a", ["x", "y"]) gives "1:a[x+y]". (7, "", ["a"]) gives "7:anon[a]".

::: question Step 2 has to happen before step 3, and the compiler is quite firm about it. Why? ..r is not a borrow and not a copy — it moves every remaining field out of r and into the new struct. That requires r to be whole. If label has already left, there is nothing coherent for ..r to take, and rustc rejects it as a use of a partially moved value.

Struct update syntax is easy to misread as “copy the rest”, especially if you have written JavaScript spread or Python **kwargs. It is not. For Copy fields it does copy; for everything else it moves, and the source struct is partially moved afterwards exactly as if you had written the field assignments out by hand.

There is a corollary worth banking: Record { ..r } with no overridden fields at all is just r written the long way, and clippy has a lint for it (unnecessary_struct_initialization, allow-by-default). :::

The lint you will meet

The starter builds the record like this:

let r = Record {
    id: id,
    label: label,
    tags: tags,
};

Once you have fixed the move errors, clippy will refuse the submission:

error: redundant field names in struct initialization
       help: replace it with: `id`

redundant_field_names is on by default, and it is the right call — when the field and the local share a name, Record { id, label, tags } says the same thing with less to read. This is a small taste of a theme that runs through the whole course: the gate is not only about correctness.

Watch out

The type must not gain a Drop impl. Give a struct a destructor and every one of these partial moves becomes E0509 instead, because a destructor is entitled to receive all of its fields intact. That is the next problem, and it is the hardest one in the beginner half of this track.

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

Loading visualization…