Reference Restrictions
Move's reference system includes strict restrictions that prevent common programming errors like data races, dangling references, and use-after-free bugs. Understanding these restrictions is crucial for writing safe Move code.
The Borrowing Rules
Move enforces three fundamental borrowing rules that must be followed at all times:
- At any given time, you can have either one mutable reference or any number of immutable references to a particular value
- References must always be valid
- References cannot outlive the data they refer to
Rule 1: Mutable vs Immutable References
This rule prevents data races by ensuring that data cannot be modified while it's being read.
Multiple Immutable References Allowed
module my_module::multiple_immutable {
public fun multiple_immutable_demo() {
let s = string::utf8(b"hello");
// Multiple immutable references are allowed
let r1 = &s;
let r2 = &s;
let r3 = &s;
// All can be used simultaneously
let len1 = string::length(r1);
let len2 = string::length(r2);
let len3 = string::length(r3);
// This is safe because no one can modify the data
let total = len1 + len2 + len3;
}
}
Only One Mutable Reference
module my_module::single_mutable {
public fun single_mutable_demo() {
let mut s = string::utf8(b"hello");
// Only one mutable reference allowed
let r1 = &mut s;
// This would cause a compilation error:
// let r2 = &mut s; // Second mutable reference
// Use the mutable reference
*r1 = *r1 + string::utf8(b" world");
}
}
Mutable and Immutable Reference Conflict
module my_module::mutable_immutable_conflict {
public fun conflict_demo() {
let mut s = string::utf8(b"hello");
// Create immutable reference
let r1 = &s;
let len = string::length(r1);
// This would cause a compilation error:
// let r2 = &mut s; // Mutable reference while immutable exists
// But this is fine - another immutable reference
let r3 = &s;
let is_empty = string::is_empty(r3);
}
}
Rule 2: References Must Always Be Valid
Move prevents dangling references by ensuring references always point to valid data.
Preventing Dangling References
module my_module::dangling_prevention {
// This would cause a compilation error - dangling reference
// fun dangle() -> &String {
// let s = string::utf8(b"hello");
// &s // s goes out of scope, making this reference invalid
// }
// Instead, return the value itself
fun no_dangle() -> String {
let s = string::utf8(b"hello");
s // Ownership transferred to caller
}
// Or return a reference to data that lives long enough
struct StringHolder has key, store {
content: String,
}
fun get_reference(holder: &StringHolder) -> &String {
&holder.content // Reference to data that outlives the function
}
}
Reference Lifetime in Structs
module my_module::struct_lifetime {
// This would cause a compilation error - reference in struct
// struct StringRef {
// content: &String, // Reference without lifetime
// }
// Instead, store the value itself
struct StringWrapper has drop, store {
content: String,
}
// Or use a different design pattern
struct StringHolder has key, store {
content: String,
}
fun create_wrapper(s: String) -> StringWrapper {
StringWrapper { content: s }
}
}
Rule 3: References Cannot Outlive Data
This rule ensures that references are always valid throughout their lifetime.
Scope and Lifetime
module my_module::scope_lifetime {
public fun scope_demo() {
let mut s = string::utf8(b"hello");
{
let r = &mut s; // Mutable reference to s
*r = *r + string::utf8(b" world");
} // r goes out of scope here
// s is still valid and can be used
let len = string::length(&s);
}
}
Function Parameter Lifetimes
module my_module::function_lifetime {
public fun function_demo() {
let s = string::utf8(b"hello");
// Pass reference to function
let len = calculate_length(&s);
// s is still valid after the function call
let is_empty = string::is_empty(&s);
}
fun calculate_length(s: &String) -> u64 {
string::length(s) // Reference is valid for the duration of the function
}
}
Reference Restrictions in Practice
Working with Collections
module my_module::collection_restrictions {
public fun vector_restrictions() {
let mut numbers = vector[1, 2, 3, 4, 5];
// Immutable borrow
let sum = calculate_sum(&numbers);
// Mutable borrow
double_values(&mut numbers);
// Can use immutable borrow again after mutable borrow ends
let new_sum = calculate_sum(&numbers);
}
fun calculate_sum(numbers: &vector<u64>) -> u64 {
let sum = 0u64;
let i = 0;
let len = vector::length(numbers);
while (i < len) {
sum = sum + *vector::borrow(numbers, i);
i = i + 1;
};
sum
}
fun double_values(numbers: &mut vector<u64>) {
let i = 0;
let len = vector::length(numbers);
while (i < len) {
let value = *vector::borrow(numbers, i);
*vector::borrow_mut(numbers, i) = value * 2;
i = i + 1;
};
}
}
Global Storage Restrictions
module my_module::global_storage_restrictions {
struct Counter has key, store {
value: u64,
}
public fun global_storage_demo(account: &signer) {
let counter = Counter { value: 0 };
move_to(account, counter);
let addr = signer::address_of(account);
// Immutable borrow from global storage
let current_value = get_counter_value(addr);
// Mutable borrow from global storage
increment_counter(addr);
// Can use immutable borrow again
let new_value = get_counter_value(addr);
}
fun get_counter_value(addr: address) -> u64 {
let counter = borrow_global<Counter>(addr);
counter.value // Return copy of value, not reference
}
fun increment_counter(addr: address) {
let counter = borrow_global_mut<Counter>(addr);
counter.value = counter.value + 1;
}
}
Common Reference Errors and Solutions
Error: Multiple Mutable References
module my_module::multiple_mutable_error {
public fun multiple_mutable_error() {
let mut s = string::utf8(b"hello");
// This would cause a compilation error:
// let r1 = &mut s;
// let r2 = &mut s; // Second mutable reference
// Solution: Use one mutable reference at a time
{
let r1 = &mut s;
*r1 = *r1 + string::utf8(b" world");
} // r1 goes out of scope
{
let r2 = &mut s;
*r2 = *r2 + string::utf8(b"!");
} // r2 goes out of scope
}
}
Error: Mutable and Immutable Reference Conflict
module my_module::conflict_error {
public fun conflict_error() {
let mut s = string::utf8(b"hello");
// This would cause a compilation error:
// let r1 = &s; // Immutable reference
// let r2 = &mut s; // Mutable reference while immutable exists
// Solution: Use immutable references first, then mutable
let len = string::length(&s);
let is_empty = string::is_empty(&s);
// Now use mutable reference
let r2 = &mut s;
*r2 = *r2 + string::utf8(b" world");
}
}
Error: Dangling Reference
module my_module::dangling_error {
// This would cause a compilation error:
// fun dangling_reference() -> &String {
// let s = string::utf8(b"hello");
// &s // s goes out of scope, making reference invalid
// }
// Solution: Return the value itself
fun return_value() -> String {
let s = string::utf8(b"hello");
s // Ownership transferred
}
// Or use a different design pattern
struct StringHolder has key, store {
content: String,
}
fun return_reference_to_stored_data(addr: address) -> &String {
let holder = borrow_global<StringHolder>(addr);
&holder.content // Reference to data that lives in global storage
}
}
Reference Restrictions and Performance
Avoiding Unnecessary Copies
module my_module::performance_restrictions {
// Good: Use references to avoid copying
public fun efficient_processing(data: &vector<u64>) -> u64 {
let sum = 0u64;
let i = 0;
let len = vector::length(data);
while (i < len) {
sum = sum + *vector::borrow(data, i);
i = i + 1;
};
sum
}
// Less efficient: Copy the entire vector
// public fun inefficient_processing(data: vector<u64>) -> u64 {
// // data is moved into the function
// // processing happens
// // data would need to be returned if needed elsewhere
// }
}
Minimizing Borrowing Conflicts
module my_module::minimize_conflicts {
public fun minimize_conflicts_demo() {
let mut data = vector[1, 2, 3, 4, 5];
// Group immutable operations together
let sum = calculate_sum(&data);
let average = calculate_average(&data);
let max = find_max(&data);
// Then do mutable operations
modify_data(&mut data);
// Can use immutable operations again
let new_sum = calculate_sum(&data);
}
fun calculate_sum(data: &vector<u64>) -> u64 {
// Implementation
0
}
fun calculate_average(data: &vector<u64>) -> u64 {
// Implementation
0
}
fun find_max(data: &vector<u64>) -> u64 {
// Implementation
0
}
fun modify_data(data: &mut vector<u64>) {
// Implementation
}
}
Best Practices for Reference Restrictions
Plan Your Borrowing Strategy
module my_module::borrowing_strategy {
public fun borrowing_strategy_demo() {
let mut data = vector[1, 2, 3, 4, 5];
// Strategy 1: Read-only operations first
let analysis = analyze_data(&data);
// Strategy 2: Single mutable operation
modify_data(&mut data);
// Strategy 3: Read-only operations after modification
let new_analysis = analyze_data(&data);
}
fun analyze_data(data: &vector<u64>) -> (u64, u64, u64) {
let sum = calculate_sum(data);
let average = calculate_average(data);
let max = find_max(data);
(sum, average, max)
}
fun modify_data(data: &mut vector<u64>) {
// Single modification operation
double_all_values(data);
}
fun calculate_sum(data: &vector<u64>) -> u64 {
// Implementation
0
}
fun calculate_average(data: &vector<u64>) -> u64 {
// Implementation
0
}
fun find_max(data: &vector<u64>) -> u64 {
// Implementation
0
}
fun double_all_values(data: &mut vector<u64>) {
// Implementation
}
}
Use Appropriate Reference Types
module my_module::appropriate_references {
// Use immutable references for read-only access
public fun read_operations(data: &String) -> (u64, bool) {
let length = string::length(data);
let is_empty = string::is_empty(data);
(length, is_empty)
}
// Use mutable references only when modification is needed
public fun modification_operations(data: &mut String) {
*data = *data + string::utf8(b" modified");
}
// Avoid unnecessary mutable references
// public fun unnecessary_mutable(data: &mut String) -> u64 {
// string::length(data) // Doesn't need mutable reference
// }
}
Handle Borrowing Conflicts Gracefully
module my_module::graceful_conflicts {
public fun graceful_conflict_demo() {
let mut data = vector[1, 2, 3, 4, 5];
// Extract values before modification to avoid conflicts
let current_sum = calculate_sum(&data);
let current_average = calculate_average(&data);
// Now modify the data
modify_data(&mut data);
// Use the extracted values
let new_sum = calculate_sum(&data);
let sum_difference = new_sum - current_sum;
}
fun calculate_sum(data: &vector<u64>) -> u64 {
// Implementation
0
}
fun calculate_average(data: &vector<u64>) -> u64 {
// Implementation
0
}
fun modify_data(data: &mut vector<u64>) {
// Implementation
}
}
Understanding and following Move's reference restrictions is essential for writing safe and correct code. These restrictions prevent common programming errors and ensure that your Move programs are memory-safe and free from data races.