Rust Programming Language's Ownership and Borrowing: Ensuring Memory Safety and Performance Together

[tr] Türkçe Oku

2023-07-11

The Uniqueness of the Rust Programming Language

Rust is a modern systems programming language that aims to offer memory safety, concurrency, and high performance together. One of the most significant features of Rust is its memory management model. This model is based on the concepts of “ownership” and “borrowing”.

The Importance of Ownership and Borrowing Concepts

Ownership and Borrowing are the cornerstones of Rust’s memory management model. These concepts enable Rust’s memory safety and concurrent operation. In this article, we will examine what these two concepts are, how they work, and how they are used in the Rust programming language in detail.

Ownership

Introduction to the Concept of Ownership

In Rust, each value has an ‘owner’. The value owned is automatically dropped when its owner goes out of scope. This is the basis of how Rust implements memory management.

The Operation of Ownership in Rust

When ownership of a variable in Rust is transferred to another variable, the first variable becomes invalid. This helps Rust prevent the “double free” memory error.

let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);

When you run this code, you will get an error. This is because the ownership of s1 has been transferred to s2, and s1 has become invalid.

Rules of Ownership

There are three main rules about ownership in Rust:

  1. Each value in Rust has an owner.
  2. At any given time, a value can have only one owner.
  3. When the owner of a value goes out of scope, the value gets dropped.

Ownership and Functions

When you pass a value to a function, the ownership of the value is transferred to the function. When the function ends, the value is dropped.

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.

    println!("{}", s);              // This will generate an error because s is no longer valid.
}

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. 

Ownership

In the above diagram, the value of the variable s defined within the main() function is moved to the takes_ownership() function. As a result of this move, the variable s is no longer valid in the main() function. When the takes_ownership() function ends, the some_string variable goes out of scope and is dropped from memory.

Ownership and Variables

In Rust, when the value of a variable is assigned to another variable, the value is not copied, the ownership is moved. However, for types that implement the Copy trait (for instance, all integer types, the boolean type, the character type, float types, and tuples), the values are copied automatically.

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);  // This will not generate an error because integers are Copy.

Ownership and Arrays

In Rust, some types such as arrays and strings are stored on the heap and their values are not automatically copied. Instead, the ownership is moved.

let a = [1, 2, 3, 4, 5];
let b = a;

println!("a = {:?}, b = {:?}", a, b);  // This will generate an error because arrays are not Copy.

The Impact of Ownership on Memory Management

In Rust, when the owner of a value goes out of scope, the value is automatically dropped, and the memory is reclaimed. This is the basis of how Rust manages memory and as a result, Rust prevents memory leaks and double free errors.

Borrowing

Introduction to the Concept of Borrowing

Borrowing allows a value to be used in Rust without transferring ownership. This can be used to read or modify a value.

The Operation of Borrowing in Rust

To borrow a value, we use the & operator. This creates a reference.

let s = String::from("hello");

let len = calculate_length(&s);

println!("The length of '{}' is {}.", s, len);

fn calculate_length(s: &String) -> usize {
    s.len()
}

In this code, we are passing a reference to the s string to the calculate_length function. This allows us to calculate the length of the s string without transferring its ownership.

Immutable Borrowing

Borrowing

In the above diagram, a reference to the s string defined within the main() function (&s) is passed to the calculate_length() function. This allows the length of the s string to be calculated without transferring its ownership.

Immutable Borrowing

When you borrow a value, you cannot modify the borrowed value. This helps Rust ensure memory safety.

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");  // This will cause an error.
}

When you run this code, you will get an error. This is because some_string is a reference and references are immutable by default.

Mutable Borrowing

To be able to modify a value, we can create a mutable reference using the mut keyword.

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

This code appends “, world” to the s string and does not produce an error because some_string is a mutable reference to the s string.

Borrowing Rules

There are two main rules about borrowing in Rust:

  1. There can only be one mutable reference to a value at a time.
  2. There cannot be both mutable and immutable references to a value at the same time.

Borrowing and Functions

When you pass a reference to a value to a function, the function cannot modify the value. However, if you pass a mutable reference, the function can modify the value.

Borrowing and Arrays

In Rust, some types, such as arrays and strings, are stored on the heap and the values of these types are not automatically copied. Instead, the values are borrowed.

let a = [1, 2, 3, 4, 5];
let b = &a;

println!("a = {:?}, b = {:?}", a, b);  // This will not cause an error, because b is a reference to a.

Relationship Between Ownership and Borrowing

Using Ownership and Borrowing Together

In Rust, we use the concept of borrowing to use a value without transferring its ownership. However, if we want to modify a value, we transfer the ownership of the value.

Borrowing-Ownership

In the above diagram, a mutable reference to the s string defined within the main() function (&mut s) is passed to the change() function. This allows the value of the s string to be modified without transferring its ownership.

Impact on Memory Safety

The concepts of ownership and borrowing ensure memory safety in Rust. Ownership guarantees that a value has only one owner, and the value is dropped when its owner goes out of scope. This prevents memory leaks and double free errors. Borrowing allows a value to be used without transferring its ownership. This ensures memory safety while also maintaining efficiency.

Common Mistakes and Their Solutions

In Rust, the most common mistakes related to ownership and borrowing are:

  1. Trying to use the initial variable after transferring ownership of a value.
  2. Trying to modify a value after taking an immutable reference to it.
  3. Trying to create both mutable and immutable references to a value at the same time.

How to Prevent These Mistakes

To prevent these mistakes, it’s important to understand and adhere to Rust’s rules of ownership and borrowing. Also, reading and understanding Rust’s error messages carefully can help. Rust’s error messages are typically very descriptive and point out what went wrong and how it can be fixed.

Practical Examples

Simple Examples Using Ownership and Borrowing

In Rust, the concepts of ownership and borrowing are often used together. Here’s an example:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;  // no problem
    let r2 = &s;  // no problem
    let r3 = &mut s; // BIG PROBLEM :)

    println!("{}, {}, and {}", r1, r2, r3);
}

When you run this code, you will get an error. This is because both immutable and mutable references to the s string have been created at the same time.

Examples for Complex Systems

Complex Systems

In the above diagram, both immutable (&s) and mutable (&mut s) references to the s string defined within the main() function are created at the same time. This is against Rust’s rules of borrowing and results in an error.

Examples for Complex Systems

In Rust, the concepts of ownership and borrowing are used to manage memory in complex systems. Here’s an example:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;  // no problem
    let r2 = &s;  // no problem
    println!("{} and {}", r1, r2);
    // r1 and r2 are no longer used after this point.

    let r3 = &mut s; // no problem now
    println!("{}", r3);
}

In this code, the r1 and r2 references become unused after the println! call. Therefore, there’s no problem in creating the r3 reference. This helps Rust ensure memory safety.

Conclusion

Summary of Rust’s Approach to Memory Management

Rust manages memory based on the concepts of ownership and borrowing. These concepts ensure memory safety and concurrent execution in Rust. Rust prevents memory leaks and double free errors by using these concepts.

How Rust Ensures Safety and Performance

Rust ensures memory safety and performance based on the concepts of ownership and borrowing. These concepts help Rust control memory management and prevent memory errors. Also, Rust does not need to automate memory management by using these concepts. This increases Rust’s performance.

Comparison with Other Programming Languages

Rust’s memory management model is different from other languages. Other languages usually rely on automatic memory management (for example, garbage collection) or manual memory management. However, Rust manages memory based on the concepts of ownership and borrowing. This ensures Rust’s memory safety and performance.

Resources

Rust Documentation and Guides



More posts like this

Rusts Drop Trait: Mastering Resource Management with Precision

2024-05-01 | #rust

Efficient resource management is pivotal in systems programming, where the reliability and performance of applications heavily depend on the meticulous management of resources. Rust’s Drop trait provides a sophisticated mechanism for deterministic resource handling, markedly enhancing the predictability and efficiency of resource management over traditional methods like garbage collection (GC) and manual resource management. The Challenge of Resource Management Resources such as memory, files, network connections, and locks are finite and critical for the stability of applications.

Continue reading 


Function Pointers in Rust: A Comprehensive Guide

2024-04-28 | #rust

Function pointers are a powerful feature in Rust, enabling developers to dynamically manage and manipulate references to functions, fostering customizable behaviors and efficient complex design patterns. This guide dives into their practical applications, syntax, advanced use cases, and best practices, and it compares them with closures and trait objects. Type Safety and Performance: A Balancing Act Rust is a language that does not compromise on safety or performance, and function pointers are no exception:

Continue reading 


Lifetime Specifiers for Tuple Structs and Enums

2024-04-17 | #rust

In Rust, lifetime specifiers are crucial for managing references and ensuring that data referenced by a pointer isn’t deallocated as long as it’s needed. Lifetime specifiers aid Rust’s borrow checker in ensuring memory safety by explicitly defining how long references within structs, enums, or functions are valid. This is especially important when dealing with non-‘static lifetimes, or data that might not live for the entire duration of the program. Below, I’ll explain how you can apply lifetime specifiers to tuple-structs and enums and then use metaphors to make the concept more engaging and intuitive.

Continue reading 