home

Rust Ownership, Move and Borrow - Part 3

Nov 22, 2021

In this short part, we'll review what we've learnt so far.

All data has one owner. Assignments (using = or passing values to a function) moves ownership. After ownership is moved, the original owner cannot be used. Having a single explicit owner means that Rust doesn't need (and thus doesn't have) a garbage collector. It is always unambiguous to the compiler when data can be be safely freed - and that's exactly what the compiler does: it inserts code to free memory at compile-time (known as Drop in Rust).

Types that implement the Copy trait are copied instead of moved.

Instead of moving data, we can also borrow data. Rust is likely to use a reference to implement borrowing, but this is an implementation detail. Regardless of how move and borrowing are implemented on a case by case basis, what matters is what the compiler enforces and guarantees with respect to moved and borrowed valued.

Along with "borrow" and "reference", you'll often see "aliasing". These are all loosely used interchangeably.

Mutability and borrowing interact in an explicit manner. You can have one mutable borrow or multiple immutable borrows. This is sometimes referred to as "Aliasing XOR Mutability" indicating that borrowing and mutability are exclusive behaviors. This is important in a world without a garbage collector, consider:

fn main() {
  let mut items = vec![1];
  let item = items.last();
  items.push(1);
  println!("{:?}", item)
}

This code fails to compile: items.last() borrows items but items.push tries to mutate items. The reason this isn't allowed is simple: mutations can render borrowed data invalid. The call to push might require new memory to be allocated resulting in previous aliased data (item in this case) to point to no-longer-valid memory. Rust doesn't allow this.

I don't think it's correct to refer to data as mutable or not. It isn't data which is mutable, but rather the bindings (aka, variables). This is obvious when you take an immutable variable and move it to a mutable variable:

fn main() {
  let a: Vec<u32> = Vec::new();

  // fails to compile, a is immutable
  a.push(1);

  // moved to b, which is mutable
  let mut b = a;
  b.push(1);
}

This is a clear example that mutability is tied to the binding, not the data.

Everything we've discussed here is the default behavior. It's the set of rules that most of your Rust code will live under. It provides the strongest safety guarantees and the lowest runtime overhead. However, Rust provides constructs to change these rules on a case by case basis. This often involves deferring compile-time checks to the runtime. Hopefully we'll get to talk about these more advanced cases soon.