26 January 2023
Ownership solves the issue of when to deallocate values, but it causes some problems for the programmer. The most obvious one is that when you pass a value with move semantics to a function, then you can’t use it afterwards. When you call a method on such a value, the same thing happens.
References are Rust’s solution to these problems.
A reference is a value that “refers” to another value, like a pointer, but with some limitations that make sure we never get a “dangling reference”, i.e. a reference that doesn’t point to a valid value. Let’s look at a simple example:
#[derive(Debug)]
struct Color { red: u8, green: u8, blue: u8 }
fn references_simple() {
let c = Color { red: 0x8c, green: 0x37, blue: 0xb1 };
let r1: &Color = &c;
let r2: &Color = &c;
println!("{} {} {}", r1.red, r2.green, c.blue);
}
The & sign is used for both reference types and values: &Color
means the type of
references to Color
, &c
means a reference to c
. In the
example, r1 and r2 both refer to c. The println on the last line uses all three variables.
That’s neat, but we were really interested in function calls. Here’s an example:
fn f1(c: &Color) {
println!("{} {} {}", c.red, c.green, c.blue);
}
fn borrowing() {
let c = Color { red: 0x8c, green: 0x37, blue: 0xb1 };
f1(&c);
println!("{}", c.red);
}
We need to put the & in both the function definition and the function call -- the Rust compiler
won’t just infer that we need a reference. After the function call we can continue using the
original variable. For a method you just use &self
:
impl Color {
fn is_black(&self) -> bool {
self.red == 0 && self.green == 0 && self.blue == 0
}
}
fn borrowing_for_method() {
let c = Color { red: 0x00, green: 0x00, blue: 0x00 };
println!("black? {}", c.is_black());
}
The compiler will make sure you don’t end up with a dangling reference. For example, moving a value while there’s still a reference to it is not allowed:
fn dangling_reference() {
let c = Color { red: 0x8c, green: 0x37, blue: 0xb1 };
let r: &Color = &c; // take a reference to c
let c2 = c; // move from c -- not allowed!
println!("{}", r.red); // use r
}
If a reference still exists after the original value is moved from or goes out of scope, the compiler will reject your program. However, if we remove the last line, the code will be accepted:
fn dangling_reference() {
let c = Color { red: 0x8c, green: 0x37, blue: 0xb1 };
let r: &Color = &c; // take a reference to c
let c2 = c; // move from c -- ok
}
Although r is still in scope when c is moved from, it’s not actually used anymore.
Like let bindings, references are immutable by default. You can take a mutable reference with
&mut
, as in this example:
fn no_red_please(c: &mut Color) {
c.red = 0;
}
fn mutable_reference() {
let mut c = Color { red: 0x8c, green: 0x37, blue: 0xb1 };
println!("original color: {:?}", c);
no_red_please(&mut c);
println!("modified color: {:?}", c);
}
One limitation is that at any point you can have either one mutable reference or any number of immutable ones. This avoids surprises where one part of your code is modifying a value through a reference and then another part of your code breaks because it didn’t expect the value to change.