References as Types

Essential
Last updated: Tags: Rust

Prerequisites

When two parts of your program need to work with the same data, you don’t want to copy it every time. References let you pass a handle to an existing value instead of the value itself — safely, without the footguns of raw pointers.

&T — shared reference

A shared reference &T gives you read-only access to a T that lives somewhere else. You can have as many &T references to the same value as you like, all at once.

let x: i32 = 42;
let r: &i32 = &x; // r points to x

r itself is a variable whose value is the address of x. It does not own the integer; it merely borrows a view of it.

&mut T — exclusive (mutable) reference

A mutable reference &mut T gives you read-write access. In exchange, you can only have one &mut T to a given value at any point — no other reference to the same value may exist at the same time.

let mut y: i32 = 10;
let m: &mut i32 = &mut y;
*m += 1; // y is now 11

The exclusivity rule is what lets Rust guarantee there are no data races and no aliased writes. The compiler enforces it; you’ll meet the details in the borrow-checker chapters.

Forming references

You create a reference with & (shared) or &mut (mutable) applied to any expression that produces a place:

let values = [1, 2, 3];
let ref_to_array: &[i32; 3] = &values;

let mut count = 0_u32;
let ref_to_count: &mut u32 = &mut count;

&expr and &mut expr are the only ways to form references. There is no separate “address-of operator” — it is just &.

Reading through a reference

To get the value a reference points to, you dereference it with *:

let n: i32 = 7;
let r: &i32 = &n;
let v = *r; // v == 7, copied out of the referent

In practice, Rust inserts automatic deref coercions in many situations, so you rarely have to write * by hand. Method calls and field accesses follow references automatically:

let s = String::from("hello");
let r: &String = &s;
let length = r.len(); // no explicit * needed; Rust inserts it

When you do need explicit * — for example, to assign through a mutable reference — write it:

let mut x = 0_i32;
let m = &mut x;
*m = 99; // assign through the reference

Size of a reference

Regardless of what T is, &T and &mut T are both exactly one usize wide — one machine pointer. The referent can be a u8 or a 1 MB struct; the reference is always pointer-sized.

use std::mem::size_of;

assert_eq!(size_of::<&u8>(),    size_of::<usize>());
assert_eq!(size_of::<&[u8; 1024]>(), size_of::<usize>());

References vs. raw pointers

Rust also has raw pointers: *const T (read-only) and *mut T (read-write). They look similar but behave very differently:

&T / &mut T*const T / *mut T
Always non-nullyesno
Guaranteed validyes (compiler checks)no
Dereference requires unsafenoyes
Borrow-checker trackedyesno

Raw pointers exist for FFI and low-level code. In ordinary Rust you use references; you reach for raw pointers only when you need to step outside the rules.

Borrow rules — intentionally deferred

References are subject to the borrow checker: a set of compile-time rules that prove no reference outlives its referent and that & and &mut to the same place are never alive simultaneously. Those rules — lifetimes, borrow scopes, the exact error messages you’ll see — are covered in dedicated checkpoints. For now, treat references purely as typed addresses.

Zig analogy

If you already know Zig, the mapping is direct:

  • &T in Rust ≈ *const T in Zig — a pointer you can only read through
  • &mut T in Rust ≈ *T in Zig (non-const pointer) — a pointer you can write through

The key difference is that Rust’s type system tracks whether a reference is alive and exclusive at compile time; Zig leaves that responsibility to you.

Summary

  • &T is a shared (read-only) reference; any number may coexist.
  • &mut T is an exclusive (read-write) reference; only one may exist at a time.
  • Form them with &expr / &mut expr.
  • Dereference with *r; Rust auto-derefs in method calls and many other positions.
  • Both are pointer-sized (usize) regardless of T.
  • References are always valid — unlike raw pointers *const T / *mut T, which require unsafe to dereference.
  • The borrow-checker rules that govern reference lifetimes and aliasing are covered separately.