Struct update syntax, and the partial move¶
Level: 101 → 201 · working knowledge
One line: ..base fills every field you did not name, by moving them one at a time. The base ends up partially dead; Copy decides which half survives.
As English: "and the rest like user1". As Rust: an assignment of each remaining field. Assignment moves.
Two things it is not¶
let b = Config { notes: "…".into(), ..a }; // VALUES from another instance of the SAME type
struct Rectangle { top_left: Point } // a TYPE, as a field's type — unrelated
Not a copy constructor either — ..base runs none of your code. Base intact and a duplicate is .clone().
The rule¶
..base moves exactly the fields you did not name — and only the non-Copy ones go dead.
| Field | Named? | Copy? |
After |
|---|---|---|---|
email |
yes | — | untouched on user1 |
active |
no | yes | copied; still readable |
sign_in_count |
no | yes | copied; still readable |
username |
no | no | moved; reading it is E0382 |
error[E0382]: borrow of moved value: `user1.username`
|
5 | let user2 = User { email: "b@example.com".to_string(), ..user1 };
| ---------------------------------------------------- value moved here
7 | println!("{}", user1.username);
| ^^^^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `user1.username` has type `String`,
which does not implement the `Copy` trait
It names user1.username, not user1. The borrow checker tracks this per field. Clearest place in the library to watch Copy work — one line copies two fields and moves a third.
Keeping the base whole¶
- Name every non-
Copyfield. Then..basecarries onlyCopyfields. ..base.clone(). The cost is written down...Default::default(). The base is a temporary nobody holds, so nothing is stranded. This is why config structs are built this way.
Syntax¶
..base comes last, no trailing comma:
error: cannot use a comma after the base struct
| ..my_instance,
| ^^^^^^^^^^^^^- help: remove this comma
|
= note: the base struct must always be the last field
If you are coming from another language¶
Python. dataclasses.replace(user1, email=…), or {**d, "email": …} for dicts.
Python copies a reference, so the string is shared and the original is untouched. Rust relocates the owned data. ..base.clone() is the line that matches Python's behaviour, and the difference between the two is the allocation Python was doing for you.
ABAP. MOVE-CORRESPONDING ls_source TO ls_target, or CORRESPONDING #( ls_source ).
Always a copy. Rust's .. looks like the same operation and is not, for any field owning heap data. Nothing in ..user1 signals that user1 lost something.
Practice¶
Predict which half survives. A struct with two Copy fields and two String fields; build a second value naming only one String. Before compiling, write down per field whether it is still readable, and why.
- Check. Then trigger the failing read and watch
E0382name the field, not the value. - Keep the base whole three ways. Which for a config struct, which for amending one record?
- Add a trailing comma after
..base— a different error from every other one here.
Solution
struct_update_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: predict which half of the base survives a `..base`.
//!
//! rustc --edition 2024 struct_update_kata.rs -o /tmp/suk && /tmp/suk
#[derive(Debug, Default)]
struct Config {
retries: u32, // Copy
verbose: bool, // Copy
host: String, // NOT Copy
notes: String, // NOT Copy
}
fn main() {
let original = Config {
retries: 12,
verbose: false,
host: "alpha".to_string(),
notes: "staging box".to_string(),
};
// Only `notes` is named, so `..original` supplies the other THREE.
let amended = Config { notes: "promoted".to_string(), ..original };
println!("Predict, field by field, what is still readable on `original`:\n");
println!(" retries Copy -> copied -> {} still readable", original.retries);
println!(" verbose Copy -> copied -> {} still readable", original.verbose);
println!(" host NOT Copy -> MOVED -> reading it is E0382");
println!(" notes NOT Copy -> not taken (we named it) -> {:?} still readable", original.notes);
println!("\n amended = {amended:?}");
println!("\nThe rule in one line:");
println!(" `..base` moves exactly the fields you did NOT name,");
println!(" and only the non-Copy ones among those actually go dead.");
println!("\nThree ways to keep the base whole:");
let base = Config { retries: 3, verbose: true,
host: "beta".to_string(), notes: "n/a".to_string() };
// 1. Name every non-Copy field yourself.
let a = Config { host: "gamma".to_string(), notes: "n/a".to_string(), ..base };
println!(" 1. name every non-Copy field -> base alive: {:?}", base.host);
// 2. Clone the base into the update position.
let b = Config { retries: 99, ..clone_config(&base) };
println!(" 2. clone into the base slot -> base alive: {:?}", base.host);
// 3. Use a temporary nobody holds.
let c = Config { host: "delta".to_string(), ..Default::default() };
println!(" 3. ..Default::default() -> nothing to strand");
println!("\n {a:?}\n {b:?}\n {c:?}");
println!("\nAnd the syntax trap, which is its own error:");
println!(" Config {{ retries: 1, ..base, }}");
println!(" error: cannot use a comma after the base struct");
println!(" note: the base struct must always be the last field");
}
// Config does not derive Clone here on purpose — this spells out that the
// "clone" in option 2 is ordinary code, not something `..` does for you.
fn clone_config(c: &Config) -> Config {
Config {
retries: c.retries,
verbose: c.verbose,
host: c.host.clone(),
notes: c.notes.clone(),
}
}
Verified output of struct_update_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
Predict, field by field, what is still readable on `original`:
retries Copy -> copied -> 12 still readable
verbose Copy -> copied -> false still readable
host NOT Copy -> MOVED -> reading it is E0382
notes NOT Copy -> not taken (we named it) -> "staging box" still readable
amended = Config { retries: 12, verbose: false, host: "alpha", notes: "promoted" }
The rule in one line:
`..base` moves exactly the fields you did NOT name,
and only the non-Copy ones among those actually go dead.
Three ways to keep the base whole:
1. name every non-Copy field -> base alive: "beta"
2. clone into the base slot -> base alive: "beta"
3. ..Default::default() -> nothing to strand
Config { retries: 3, verbose: true, host: "gamma", notes: "n/a" }
Config { retries: 99, verbose: true, host: "beta", notes: "n/a" }
Config { retries: 0, verbose: false, host: "delta", notes: "" }
And the syntax trap, which is its own error:
Config { retries: 1, ..base, }
error: cannot use a comma after the base struct
note: the base struct must always be the last field
The verified output¶
Verified output of struct_update.rs — regenerated by tools/run_examples.py, never hand-typed.
1. What it saves you
user2 = User { email: …, ..user1 }
User { active: true, sign_in_count: 1, username: "someusername123", email: "another@example.com" }
`..base` must come LAST, and takes no trailing comma.
2. It is an assignment, so it MOVES — and it moves per FIELD
user1.active true <- bool is Copy, so it was copied
user1.sign_in_count 1 <- u64 is Copy, so it was copied
user1.username -- moved out, and now unusable:
error[E0382]: borrow of moved value: `user1.username`
note: move occurs because `user1.username` has type `String`,
which does not implement the `Copy` trait
user1.email someone@example.com <- NOT moved: user2 supplied its own
user2.username someusername123 <- this is user1's String, relocated
So `user1` is not dead. It is PARTIALLY moved, field by field.
3. Name every non-Copy field and the base survives intact
base is still whole: User { active: true, sign_in_count: 7, username: "ada", email: "ada@example.com" }
and the new one: User { active: true, sign_in_count: 7, username: "ben", email: "ben@example.com" }
4. `..Default::default()` never strands anything
User { active: false, sign_in_count: 0, username: "cara", email: "" }
The base is a temporary nobody holds, so there is no binding
left half-moved. That is why config structs use this form.
5. It is not a copy constructor
`..base` does not clone, and it does not call any of your code.
If you want `base` intact and a full duplicate, that is `.clone()`.
See also¶
Po polsku¶
Polska nazwa myli od pierwszego słowa: struct update syntax tłumaczy się na „składnię aktualizacji struktury”, a ..base niczego nie aktualizuje — buduje nową wartość i przy okazji potrafi wypatroszyć starą. Po ludzku ..user1 znaczy „a reszta jak w user1”, ale w Ruscie jest to przypisanie każdego niewymienionego pola, a przypisanie oznacza przeniesienie własności. Warto też od razu odróżnić te dwie kropki od zakresu: 1..5 a ..base w literale struktury to zupełnie inna składnia mimo tego samego znaku. I nie jest to żaden konstruktor kopiujący — ..base nie uruchamia ani linijki twojego kodu; jeśli chcesz mieć bazę nietkniętą i pełny duplikat, to .clone().
O tym, która połowa bazy przeżyje, decyduje Copy. Pola Copy (bool, u64) zostają skopiowane i dalej dają się czytać; String zostaje przeniesiony, a próba odczytu kończy się E0382. Najciekawsze jest to, na co wskazuje komunikat: „borrow of moved value: user1.username” — nie user1, tylko konkretne pole. borrow checker śledzi przeniesienia pole po polu, więc struktura bywa przeniesiona częściowo i pozostaje w części używalna. Trudno o jaśniejsze miejsce, żeby zobaczyć Copy przy pracy: jedna linijka kopiuje dwa pola i zabiera trzecie.
Bazę da się zachować w całości na trzy sposoby, a wybór zależy od zadania. Do struktur konfiguracyjnych bierze się ..Default::default() — baza jest wtedy wartością tymczasową, której nikt nie trzyma, więc nie ma czego osierocić. Poprawiając pojedynczy rekord, wymień z nazwy wszystkie pola nie-Copy, a ..base dokopiuje już tylko te Copy. Trzecia droga to ..base.clone(), czyli koszt zapisany wprost w kodzie. Na koniec drobiazg składniowy z własnym komunikatem: ..base musi stać na końcu i nie znosi przecinka po sobie — „cannot use a comma after the base struct”.
Szukaj po polsku: składnia aktualizacji struktury · częściowe przeniesienie własności · Copy kontra Clone · rust struct update syntax partial move · rust E0382 borrow of moved value