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 that object. That variable is said to Own that object.
Eg:let p = Person {age: 30};
// only p can grant write access to the Person object.
Only one owner can exist for an object at any point, and the object’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
/* 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);
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!
Given that we have established four types of entities (Read-Only/Read-Write owners/refs), the table and state diagram below lay out how the assignment operation across all of them work. 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 | Try It | |
1 | Read Only Owner | Read Only Owner | Permanently invalidated | Link |
2 | Read Only Owner | ReadWrite Owner | Permanently invalidated | Link |
3 | ReadWrite Owner | Read Only Owner | Permanently invalidated | Link |
4 | ReadWrite Owner | ReadWrite Owner | Permanently invalidated | Link |
5 | Read Only Owner | Read Only Ref | Becomes RO, Non-Movable as long as dest is in use | Link |
6 | Read Write Owner | Read Write Ref | Becomes unusable as long as dest is in use | Link |
7 | Read Write Owner | Read Only Ref | Becomes RO, Non-Movable as long as dest is in use | Link |
You can find the code for the above examples in this Github repository: rust-ownership-model