11.3 Struct Update Syntax: ..other_struct
Right, let’s talk about one of my favorite quality-of-life features in Rust: the struct update syntax. You’ve just defined a new struct instance, and you realize that half of its fields need to be the exact same values as another instance you already have. Your first instinct, coming from lesser languages, might be to manually assign each field. Don’t do that. It’s tedious, it’s error-prone, and frankly, it offends my sensibilities. This is where ..other_struct swoops in to save the day.
The syntax is brilliantly simple. You’re creating a new struct, and you tell the compiler: “For any field I haven’t explicitly set, just go look at other_struct for the value.” It’s like photocopying a form and then just filling in the blanks that changed.
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// Now let's create user2, who is mostly the same as user1
// but has a different email and a fresh sign_in_count.
let user2 = User {
email: String::from("another@example.com"),
sign_in_count: 0,
..user1
};
In this example, user2 gets its username and active fields directly from user1. We only had to specify the two fields that were different. This is not only cleaner but also far safer. If we later add a new field to the User struct, say profile_picture: String, our update syntax code will continue to work without modification, pulling the profile_picture from user1. Manual assignment would have blown up at compile time, forcing us to handle the new field.
It’s a Move, Not a Copy
Here’s the first “gotcha,” and it’s a big one if you’re not paying attention. The update syntax ..user1 moves the data from user1. Notice in our example above that all the fields were simple types that implement Copy (u64, bool) or were Strings we were explicitly providing for user2. What happens if we try to use user1 after creating user2?
// This will NOT compile.
let user2 = User {
email: String::from("another@example.com"),
..user1 // username is moved from user1 to user2 here
};
println!("{}", user1.username); // Error! user1.username was moved.
Boom. The compiler stops you dead in your tracks, and rightly so. user1.username is a String, which does not implement Copy. When we use ..user1, the username field is moved into user2. Trying to access user1.username afterward is a classic use-after-move error. The fix is usually to borrow or clone, but often, if you’re updating from another instance, it implies you’re done with the original. Plan your data flow accordingly.
The Order of Operations Matters… Psych!
Here’s a fun bit of trivia that almost never comes up but is good to know: the explicit field assignments happen before the update syntax. This means you can actually use the update syntax to overwrite a field you just set. Please, for the love of all that is holy, don’t do this. It’s a fantastic way to confuse the next person reading your code (who is probably you, at 2 AM).
// This is valid but utterly deranged. Don't do it.
let confused_user = User {
active: false, // Set active to false...
..user1 // ...whoops, user1.active was true, so now it's true again.
};
The result is that confused_user.active will be true. The ..user1 application wins. The compiler allows this madness because it’s technically unambiguous from its perspective, but it’s a clear sign you’ve lost the plot. Always structure your code so the update syntax provides the defaults and your explicit assignments are the intentional overrides.
Best Practice: Use it with Constructor Functions
This syntax truly shines when paired with constructor functions or default values. Imagine a common scenario where you have a default configuration and you want to create a new one that only changes a few values.
impl User {
fn new(email: String, username: String) -> Self {
Self {
email,
username,
active: true,
sign_in_count: 0,
}
}
}
// Now creating a user with a custom flag is a one-liner.
let inactive_user = User {
active: false,
..User::new("new@user.com".to_string(), "new_user".to_string())
};
This pattern is incredibly powerful. It lets you define a sensible baseline and then make targeted adjustments without repeating yourself. It’s the kind of elegant, DRY code that makes Rust a joy to work with once you get the hang of it. Just remember the move semantics, and you’ll avoid 99% of the headaches.