In Rust, the concept of borrowing is a fundamental part of its ownership model. It allows you to share access to data without transferring ownership. This tutorial will cover the rules and lifetimes associated with borrowing in Rust, providing both theoretical understanding and practical examples.
Borrowing in Rust is managed through references (&). References allow you to refer to some value without taking ownership of it. There are two types of references:
&T): These allow multiple immutable borrows at the same time, but no mutable borrow can exist simultaneously.&mut T): These allow a single mutable borrow at a time, and no other borrows (immutable or mutable) can exist simultaneously.The key rule of borrowing is that you cannot have both mutable and immutable references to the same data in the same scope. This ensures memory safety without needing a garbage collector.
Let's start with an example of immutable borrowing:
1fn main() {2let s1 = String::from("hello");34let len = calculate_length(&s1);56println!("The length of '{}' is {}.", s1, len);7}89fn calculate_length(s: &String) -> usize {10s.len()11}
In this example:
&s1 creates an immutable reference to s1.calculate_length takes a reference to a String and returns its length.s1 remains valid after the function call.Now, let's see how mutable borrowing works:
1fn main() {2let mut s = String::from("hello");34change(&mut s);56println!("The modified string is '{}'.", s);7}89fn change(s: &mut String) {10s.push_str(", world!");11}
In this example:
&mut s creates a mutable reference to s.change takes a mutable reference and modifies the string.Rust enforces strict rules about mixing immutable and mutable references. Here's an example that will cause a compile-time error:
1fn main() {2let mut s = String::from("hello");34let r1 = &s; // no problem5let r2 = &s; // no problem6let r3 = &mut s; // BIG PROBLEM78println!("{} and {}", r1, r2);9println!("{}", r3);10}
In this example:
r1 and r2 are immutable references to s, which is allowed.r3 after the immutable references causes a compile-time error because Rust cannot ensure that the data is not being read while it's being modified.Lifetimes in Rust are another crucial aspect of borrowing. They describe how long references are valid. Here's an example with explicit lifetimes:
1fn main() {2let string1 = String::from("abcd");3let string2 = "xyz";45let result = longest(string1.as_str(), string2);6println!("The longest string is {}", result);7}89fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {10if x.len() > y.len() {11x12} else {13y14}15}
In this example:
longest takes two string slices with the same lifetime 'a.Now that you understand borrowing rules and lifetimes in Rust, you can explore more advanced topics such as trait objects, closures, and async programming. For further reading:
These resources will provide deeper insights into Rust's ownership model and help you write safer and more efficient code.