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:
- Each value in Rust has an owner.
- At any given time, a value can have only one owner.
- 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.
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
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:
- There can only be one mutable reference to a value at a time.
- 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.
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
Common Mistakes Related to Ownership and Borrowing
In Rust, the most common mistakes related to ownership and borrowing are:
- Trying to use the initial variable after transferring ownership of a value.
- Trying to modify a value after taking an immutable reference to it.
- 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
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.