Lesson 5
Ownership and Borrowing in Rust
Introduction

Welcome to this lesson on Ownership and Borrowing in Rust. In previous lessons, we've established a strong foundation in core Rust concepts like structs, enums, generics and traits. Now, we'll delve into Rust's unique approach to memory safety: Ownership and Borrowing. Understanding these concepts is crucial as they form the backbone of safe and efficient programming in Rust, allowing you to write robust applications with confidence.

Memory Management in Rust

Before we dive in, let's briefly discuss how Rust manages memory using the stack and the heap:

  • Stack: Used for storing data with a known, fixed size at compile time. It's fast for allocation and deallocation.
  • Heap: Used for data that can change size or when the size is not known at compile time, like String. It requires explicit memory management.

Ownership rules in Rust govern how heap-allocated data is managed, preventing issues like dangling pointers and data races at compile time.

Exploring Ownership in Rust

Ownership in Rust is a set of rules governing how a program manages memory. These rules ensure clean memory usage and prevent issues like dangling references. There are three main rules of ownership:

  1. Each value in Rust has a variable that's its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped.

Consider the following function that takes ownership of a String:

Rust
1fn takes_ownership(some_string: String) { 2 println!("Owned string: {}", some_string); 3}

In this code:

  • some_string takes ownership of the String parameter passed to it.
  • The ownership moves to some_string, and when it goes out of scope at the end of the function, the String is dropped.

For instance:

Rust
1fn main() { 2 let s = String::from("Hello"); 3 takes_ownership(s); 4 // 's' is no longer valid here as ownership has been transferred 5 // println!("{}", s); // This would cause a compile-time error 6}

When takes_ownership is called, s is moved into the function and dropped when it leaves the scope. Attempting to use s after it has been moved results in a compile-time error, ensuring memory safety by preventing access to dropped data.

Copy vs Move Semantics

Ownership in Rust is tightly linked with the concepts of move and copy semantics. Types in Rust can be either Copy or Move types.

  • Copy types: Simple, fixed-size types that can be duplicated cheaply. When you assign or pass these types (like numbers, booleans, or characters), Rust automatically makes a copy, leaving the original value intact.
  • Move types: Complex types that manage resources (like String or Vec<T>). When you assign or pass these types, Rust transfers ownership from one variable to another instead of copying the data, ensuring there's always exactly one owner.

Let's explore this with the i32 type, which implements the Copy trait. Consider the example:

Rust
1fn makes_copy(some_integer: i32) { 2 // `i32` implements `Copy`, so it is duplicated, not moved. 3 println!("Copied integer: {}", some_integer); 4} 5fn main() { 6 let x = 5; 7 makes_copy(x); 8 // 'x' remains valid here because it is copied, not moved 9 println!("x is still accessible: {}", x); 10}

In this case, x is still valid after the function call because integers implement Copy, so x is copied, not moved.

Borrowing with References

Borrowing in Rust allows you to reference a value without taking ownership. This is done using references, denoted by &. Here are the fundamental rules of references:

  1. At any given time, you can have either one mutable reference or any number of immutable references.
  2. References must always be valid; they must not be dangling.

Here's an example of a function to calculate the length of a string by borrowing it as an immutable reference:

Rust
1fn calculate_length(s: &String) -> usize { 2 // &String allows read-only access to `s` 3 s.len() 4} 5 6fn main() { 7 let s1 = String::from("Hello"); 8 let len = calculate_length(&s1); 9 println!("The length of '{}' is {}.", s1, len); 10}

The calculate_length function borrows s1 (via &String) without taking ownership, allowing it to remain valid outside the function since ownership hasn't been transferred.

Attempting to violate the borrowing rules results in a compile-time error. For instance:

Rust
1fn main() { 2 let s = String::from("Hello"); 3 let r1 = &s; 4 let r2 = &s; 5 let r3 = &mut s; // Error: cannot borrow `s` as mutable because it is also borrowed as immutable 6}

This error ensures that data races are prevented at compile time.

Slices: Borrowing Sections of Data

Slice types, like &str and &[T], are a form of referencing parts of data without ownership. They are commonly used in Rust code and demonstrate borrowing effectively.

For example:

Rust
1fn first_word(s: &String) -> &str { 2 let bytes = s.as_bytes(); 3 for (i, &item) in bytes.iter().enumerate() { 4 if item == b' ' { 5 return &s[0..i]; 6 } 7 } 8 &s[..] 9} 10 11fn main() { 12 let s = String::from("Hello world"); 13 let word = first_word(&s); 14 println!("First word: {}", word); 15}

In this code:

  • &str is a string slice, borrowing a part of the String.
  • We avoid taking ownership and can work efficiently with parts of data.
Working with Mutable References

Mutable references allow you to borrow and modify data. Rust enforces strict rules to ensure safety:

  1. You can have only one mutable reference to a particular piece of data at a time.
  2. This prevents data races and ensures memory safety.

Consider a function that changes a string:

Rust
1fn change(s: &mut String) { 2 s.push_str(", world!"); 3} 4 5fn main() { 6 let mut s2 = String::from("Hello"); 7 change(&mut s2); 8 println!("After mutation: {}", s2); 9}

The function in this code borrows a mutable reference to a String with &mut String, allowing it to safely modify the data.

Attempting to have multiple mutable references leads to a compile-time error:

Rust
1fn main() { 2 let mut s = String::from("Hello"); 3 let r1 = &mut s; 4 let r2 = &mut s; // Error: cannot borrow `s` as mutable more than once at a time 5 println!("{}, {}", r1, r2); 6}

This restriction prevents simultaneous modification of data, eliminating data races.

Common Ownership Patterns

Understanding when to use ownership, borrowing, and cloning is key to writing efficient Rust code.

  • Using References in Function Parameters: Functions often take references to avoid taking ownership, allowing the caller to retain ownership of the data.

    Rust
    1fn print_string(s: &String) { 2 println!("{}", s); 3}
  • Cloning Data: When you need to retain ownership of a value but still need to provide it to a function that takes ownership, you can clone it.

    Rust
    1fn takes_ownership(s: String) { 2 println!("Owned string: {}", s); 3} 4 5fn main() { 6 let s = String::from("Hello"); 7 takes_ownership(s.clone()); 8 println!("Original string: {}", s); 9}
  • Choosing Between Owned Values and References: Use references when you don't need ownership, and owned values when you need to transfer ownership or ensure data lives long enough.

Summary and Preparation for Practice

In this lesson, we've explored the fundamental concepts of ownership and borrowing in Rust. You now understand how ownership is transferred between variables, how borrowing enables data use without ownership transfer, and the critical rules that ensure memory safety. From move semantics to mutable references, and from slices to common ownership patterns, these principles form the backbone of safe and efficient Rust programming.

Congratulations on reaching the end of this "Fundamental Rust Concepts for Design Patterns" course! As you move forward to the practice exercises, you'll have the opportunity to apply these concepts hands-on, solidifying your understanding of Rust's unique approach to memory management. Happy coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.