When Ferrous Metals Corrode, pt. III

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 system

  • There are refcounted types: Rc and Arc

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() {
    g(x); // bad: x would be moved in first iteration,
          // 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 {
    s.push('!');
    println!("{}", 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.