Learn Rust: Assignment and Memory Semantics
If you are familiar with Java or C/C++, understanding the assignment operator (=) in Rust will demystify a lot of its memory semantics. Assignment in Rust can have a range of outcomes depending on what you are assigning from and to, and on whether you are using the reference operator (&) and the mut keyword (short for mutable). In this post I will describe some scenarios of the assignment operation in Rust.
But first, two definitions:
- When you instantiate an object and assign it to a variable, only that variable controls write access privileges to the memory region associated with that object. This is called Ownership.
Eg:let p = Person {age: 30};
// only p can grant write access to the Person object.
Only one owner can exist for a memory region at any point, and the memory region's destructor is called when that (sole) owner goes permanently out of scope. - You can create multiple references (a.k.a aliases) to an existing object, not too unlike C/C++ pointers or references. References won’t invoke the destructor even after they go out of scope.
Eg:let p_ref = &p;
// p_ref is a (read only) reference to the Person object above
What is very different in Rust compared to C/C++ and Java/Go?
- If you have a class (like Person above) and do not explicitly implement the Copy trait, assigning a new variable to an existing owner variable makes the older variable unusable from then on!
Eg:let p = Person {age: 30};
let mut q = p; // Create a new, read-write owner for p's memory. Any use of p after this line is a compiler error! - Whether a line of code is valid can depend on what subsequent lines of the program do!
Eg:/* Line 1 */ let mut p = Person {age: 30}; // Create a read-write owner variable
In the above, Line 4 flags a compiler error. You cannot mutate Person on Line 4, because the compiler detects that an already created Read-Only ref will be used even after the mutation. If you comment out Line 5, the compiler figures out that the Read-Only ref is not used beyond Line 3, and will let you mutate on Line 4. Observe how different this is from the scoping rules you may be used to in other languages!
/* Line 2 */ let p_ro_ref = &p; // Create a read-only ref to p
/* Line 3 */ println!("{}", p_ro_ref.age);
/* Line 4 */ p.age += 1; // NOTE: This line won't compile because of the next line!
/* Line 5 */ println!("{}", p_ro_ref.age);
Given that we have established four types of entities (Read-Only/Read-Write owners/refs), the table (apologize for the formatting) and state diagram below lay out how the assignment operation works across all of them. Each row also contains a link to relevant code in the Rust playground where you can try it out.
If you are new to Rust, take some time to evaluate whether each behaviour below makes logical sense and how they fit in together.
|Assign From | Assign To | Effect on Source var | Link to Try|
— — — — — — — — — — — — — — — — — — — — — —
Read Only Owner | Read Only Owner | Permanently invalidated | Link
Read Only Owner | ReadWrite Owner | Permanently invalidated | Link
ReadWrite Owner | Read Only Owner | Permanently invalidated | Link
ReadWrite Owner | ReadWrite Owner | Permanently invalidated | Link
Read Only Owner | Read Only Ref | Becomes RO, Non-Movable till Dest can’t be expired | Link
Read Write Owner | Read Write Ref | Becomes unusable till Dest can’t be expired | Link
Read Write Owner | Read Only Ref | Becomes RO, Non-Movable till Dest can’t be expired | Link
Each arrow represents an assignment going from the right side to the left side of an assignment statement
You can find the code for the above examples in this Github repository: rust-ownership-model