Intro
This part summarizes the fourth chapter of "Programming Rust, 2nd Edition" and deals with data ownership.
This to me is one of the most fascinating things in Rust: how it strictly tracks which part of the code holds a piece of data, and how that ownership is passed around – with the aim of being both performant and safe. Performant, because no garbage collection is required; and safety through automatic memory management.
Ownership
Every value has a single owner that determines its lifetime – freeing the owner (e.g. a variable that goes out of scope) frees the value too.
Python manages memory by keeping a refcount, and garbage-collecting it when the refcount goes to zero. Rust variables (in principle) have only one referent, so this makes it easier to determine when it should be freed. Ownership is recursive, trees own their (leaf) nodes, structs their fields, vectors their elements etc.
Value ownership can be moved from one owner to another.
Simple types (integers, chars etc.) are
Copy
types, those are not part of the ownership systemThere are refcounted types:
Rc
andArc
Finally, values can be "borrowed" from their owners. References are non-owning pointers with limited lifetimes.
Moves
Assigning a value in Rust does not copy but move – the RHS of an assignment becomes uninitialized (except Copy types, see below). Move means moving ownership here. This way the one referent per value rule is kept.
For vectors and hashmaps there is the .clone()
method that copies values – these then are separate, and have separate owners.
Operations that move ownership:
Assignment
Returns from functions
Constructing new values
Passing func args
For instance, this loop will not compile, as the variable x
could be moved into g()
more than once (even if deciderfunc()
only ever would return false).
let x = vec!["non", "copy", "type"];
while deciderfunc() {
; // bad: x would be moved in first iteration,
g(x)// uninitialized in second
}
If x
were a copy type the situation would be different though.
Moves and Indexed Content
Rust also prohibits parts of collections to become uninitialized, e.g. it's forbidden to move elements out of vectors.
It is possible to pop off the last elem of a vector, as well as swap in other values instead however.
Also, iterating over a vector takes ownership of it:
let v = vec!["a".to_string(), "b".to_string()];
for mut s in v {
.push('!');
sprintln!("{}", s);
}
If this is not wanted typically what you need is to either borrow a ref &v
, or clone the vector altogether.
Copy Types: The Exception to Moves
As noted above, Copy types (e.g. ints, floats, chars and other non-expensive types) are not moved but copied.
User defined types such as structs and enums can be marked Copy iff their components are Copy:
#[derive(Copy, Clone)]
struct Label { number: u32 }
Warning: marking a type Copy means client code can do a lot of things that would not be possible w/o having it Copy. Changing it later to non-Copy is a very breaking change
Rc and Arc: Shared Ownership
Rc and Arc (which is Rc plus thread safety) are refcounted pointers.
This is how to use Rc:
use std::rc::Rc;
let s: Rc<String> = Rc::new("shirataki".to_string());
let t: Rc<String> = s.clone();
After this, t
points to the same "shirataki" string from s
, only the refcount increases.
All the Rc values are immutable.
Coda
The concept of owning a piece of data is something I haven't come across elsewhere, and I have a feeling it'll take me some getting used-to. The idea makes perfect sense to me though: increase safety by preventing all and sundry writing to some location. That very much seems in line with Rusts safety-mindedness elsewhere.