Beginner Level
Ownership and Borrowing
Chapter 5: Ownership and Borrowing š
Ownership is Rust's most unique feature and what makes it memory-safe without a garbage collector. Understanding ownership is crucial to mastering Rust!
What is Ownership?
Ownership is a set of rules that governs how Rust manages memory:
- Each value in Rust has an owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped
The Stack and the Heap
Stack
- Fast access
- Fixed size data
- LIFO (Last In, First Out)
- Examples: integers, booleans, chars
Heap
- Slower access
- Variable size data
- Allocated at runtime
- Examples: String, Vec, HashMap
Variable Scope
fn main() {
{ // s is not valid here, it's not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
}String Type
String Literals vs String Objects
fn main() {
let s1 = "hello"; // String literal (&str) - immutable
let s2 = String::from("hello"); // String object - mutable, heap-allocated
let mut s3 = String::from("hello");
s3.push_str(", world!"); // This works!
println!("{}", s3);
}Move Semantics
Simple Types (Copy)
fn main() {
let x = 5;
let y = x; // x is copied to y, both are valid
println!("x = {}, y = {}", x, y); // This works!
}Complex Types (Move)
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2, s1 is no longer valid
// println!("{}", s1); // This would cause a compile error!
println!("{}", s2); // This works
}Clone
If you want to deeply copy heap data:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy
println!("s1 = {}, s2 = {}", s1, s2); // Both are valid!
}Ownership and Functions
Passing Values to Functions
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it's okay to still
// use x afterward
println!("x is still valid: {}", x);
// println!("{}", s); // This would error!
}
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.Return Values and Scope
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.
fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}References and Borrowing
Instead of taking ownership, you can borrow values:
Immutable References
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Pass a reference
println!("The length of '{}' is {}.", s1, len); // s1 is still valid!
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
// it refers to, it is not dropped.Mutable References
fn main() {
let mut s = String::from("hello");
change(&mut s); // Pass a mutable reference
println!("{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}Rules of References
- At any given time, you can have either one mutable reference or any number of immutable references
- References must always be valid
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point
let r3 = &mut s; // no problem
println!("{}", r3);
}Dangling References
Rust prevents dangling references at compile time:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger! This won't compile!Fix by returning the String directly:
fn no_dangle() -> String {
let s = String::from("hello");
s // Return ownership
}Slice Type
Slices let you reference a contiguous sequence of elements:
String Slices
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // or &s[..5]
let world = &s[6..11]; // or &s[6..]
let whole = &s[..]; // entire string
println!("{} {} {}", hello, world, whole);
}Array Slices
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
}Practical Examples
Example 1: First Word Function
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
println!("First word: {}", word);
// s.clear(); // This would error because word is still borrowing s
}Example 2: Largest Element
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
}Common Ownership Patterns
Pattern 1: Take and Return
fn process_string(s: String) -> String {
// Process the string
format!("Processed: {}", s)
}Pattern 2: Borrow and Process
fn process_string_ref(s: &str) -> String {
// Process without taking ownership
format!("Processed: {}", s)
}Pattern 3: Mutable Borrow
fn modify_string(s: &mut String) {
s.push_str(" - modified");
}Practice Exercises
Exercise 1: String Manipulation
Write a function that takes a string and returns it with all vowels removed:
fn remove_vowels(s: &str) -> String {
// Your implementation here
}Exercise 2: Vector Operations
Write a function that finds the second largest number in a vector:
fn second_largest(numbers: &[i32]) -> Option<i32> {
// Your implementation here
}Key Takeaways
- ā Ownership prevents memory leaks and dangling pointers
- ā Values are moved by default, copied if they implement Copy
- ā Use references (&) to borrow without taking ownership
- ā Mutable references (&mut) allow modification
- ā Can't have mutable and immutable references simultaneously
- ā Slices reference parts of collections safely
Ready for Chapter 6? ā Structs