References and Borrowing
Ownership is powerful, but constantly moving values in and out of functions gets tedious. Fortunately, Oxide has a feature for using values without transferring ownership: references.
A reference allows you to refer to a value without taking ownership of it. Instead of passing the value itself, you pass a reference to it. When the function returns, ownership remains with the original owner.
Creating and Using References
You create a reference using the ampersand (&) operator:
fn main() {
let s = String.from("hello")
let length = calculateLength(&s)
println!("'{}' has length {}", s, length) // s is still valid!
}
fn calculateLength(s: &String): UIntSize {
s.len()
} // s goes out of scope, but it doesn't own the String, so nothing happens
The &s syntax creates a reference to s. The calculateLength function receives a reference (&String) instead of the string itself. Since calculateLength doesn't own the string, the string is not dropped when the function returns.
Notice that we can still use s after calling calculateLength. The original owner retains ownership; we only loaned it to the function.
Mutable References
By default, references are immutable. If you try to modify the data through a reference, the compiler stops you:
fn main() {
let s = String.from("hello")
// changeString(&s) // Error: cannot mutate through immutable reference
changeString(&mut s) // Error: s is immutable
}
fn changeString(s: &String) {
// s.push_str(" world") // Error: cannot mutate through immutable reference
}
To modify data through a reference, you need a mutable reference, declared with &mut:
fn main() {
var s = String.from("hello")
changeString(&mut s)
println!("\(s)") // "hello world"
}
fn changeString(s: &mut String) {
s.push_str(" world")
}
Important: The original binding must be mutable (var s) to allow mutable references. You cannot create a mutable reference to an immutable binding.
The Rules of Borrowing
Oxide's borrow checker enforces two critical rules:
- Either multiple immutable references OR one mutable reference at a time
- References must always be valid (no use-after-free)
Let's explore these rules with examples.
Multiple Immutable References
You can have multiple immutable references to the same data:
fn main() {
let s = String.from("hello")
let r1 = &s
let r2 = &s
let r3 = &s
println!("\(r1), \(r2), \(r3)") // All three references work fine
}
This is safe because all readers are immutable. Multiple readers cannot corrupt data.
Immutable and Mutable References Cannot Coexist
Once you create a mutable reference, you cannot have any immutable references:
fn main() {
var s = String.from("hello")
let r1 = &s
let r2 = &s
// var r3 = &mut s // Error: cannot borrow as mutable while already borrowed as immutable
println!("\(r1), \(r2)")
}
The compiler prevents the mutable reference because r1 and r2 are still in use. If we allowed &mut s, code using r1 or r2 might suddenly see the data change, which would be surprising and dangerous.
Using Scope to Release Borrows
A borrow ends when the reference is last used, not necessarily when it goes out of scope:
fn main() {
var s = String.from("hello")
let r1 = &s
let r2 = &s
println!("\(r1), \(r2)") // Last use of r1 and r2
// r1 and r2 are no longer needed after this point
var r3 = &mut s // OK: r1 and r2's borrows have ended
r3.push_str(" world")
println!("\(r3)")
}
This is called non-lexical lifetimes (NLL). Rust introduced this feature to make borrowing less restrictive. Oxide inherits it, which means you often get mutable access sooner than you might expect.
Mutable References Are Exclusive
Only one mutable reference can exist at a time:
fn main() {
var s = String.from("hello")
var r1 = &mut s
// var r2 = &mut s // Error: cannot have two mutable references
r1.push_str(" world")
println!("\(r1)")
}
This rule prevents data races and ensures that if you modify data, no other code can see it in an inconsistent state.
The &str Lightweight Reference
We've seen &str in function parameters. This is a reference to a string slice, which we'll explore in the next section. For now, understand that &str borrows a string—it's lighter weight than &String because it doesn't own any heap allocation:
fn main() {
let s = String.from("hello world")
let word = firstWord(&s)
println!("\(word)") // "hello"
}
fn firstWord(s: &str): &str {
let bytes = s.as_bytes()
for (i, &item) in bytes.iter().enumerate() {
if item == ' ' {
return &s[0..i]
}
}
&s[..]
}
Using &str is more flexible than &String because it accepts both String references and string literals.
Why Borrowing Matters
The borrowing system solves the ownership problem we encountered earlier. Instead of constantly moving values and returning them, you can borrow them:
// Without borrowing: tedious
fn main() {
let s1 = String.from("hello")
let (s1, len) = calculateLength(s1)
println!("'{}' has length {}", s1, len)
}
fn calculateLength(s: String): (String, UIntSize) {
let length = s.len()
(s, length)
}
// With borrowing: clean
fn main() {
let s1 = String.from("hello")
let len = calculateLength(&s1)
println!("'{}' has length {}", s1, len)
}
fn calculateLength(s: &String): UIntSize {
s.len()
}
Borrowing lets functions use data without taking responsibility for it.
Mutable References Enable Controlled Mutation
Mutable references are Oxide's way of saying "this function needs to modify this data." They make your code's intent clear:
fn main() {
var user = User { name: "Alice".toString() }
updateUserName(&mut user, "Bob")
println!("\(user.name)") // "Bob"
}
fn updateUserName(user: &mut User, newName: &str) {
user.name = newName.toString()
}
The &mut syntax makes it obvious that the function will modify the argument. This is much clearer than passing a regular parameter and having side effects.
Lifetime Annotations
Sometimes the compiler needs you to explicitly specify how long a reference is valid. This is called a lifetime. We'll explore lifetimes in depth in a later chapter, but here's a simple example:
fn longest<'a>(x: &'a str, y: &'a str): &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let s1 = "short"
let s2 = "a much longer string"
let result = longest(s1, s2)
println!("\(result)")
}
The 'a notation tells the compiler that the returned reference lives as long as both input references. This ensures the return value is always valid.
You don't need to understand lifetimes yet. Just know that Oxide sometimes requires you to be explicit about how long references live, which is another safety feature.
Rust Comparison
The referencing and borrowing system is identical to Rust. The same &T and &mut T syntax, the same rules, the same benefits:
#![allow(unused)] fn main() { // Rust - identical to Oxide fn calculate_length(s: &String) -> usize { s.len() } fn change_string(s: &mut String) { s.push_str(" world"); } }
Summary
- References allow using values without taking ownership
- Immutable references (
&T) are the default - Mutable references (
&mut T) allow modification, with restrictions - Oxide enforces: either many immutable references OR one mutable reference
- Borrows end when the reference is no longer used (non-lexical lifetimes)
- References prevent data races and use-after-free bugs at compile time
- Lifetime annotations sometimes explicit about how long references live
Borrowing is one of Oxide's most powerful features. It lets you express ownership clearly while remaining flexible about how data flows through your program. Combined with ownership, it enables memory safety without garbage collection—the same achievement Rust pioneered.
In the next section, we'll explore a special kind of reference: slices.