English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Computer programs must manage the memory resources they use during runtime.
Most programming languages have the function of managing memory:
C/C++ Such languages mainly manage memory through manual methods, and developers need to manually apply for and release memory resources. However, in order to improve development efficiency, many developers do not have the habit of releasing memory in time, as long as it does not affect the realization of the program's functions. Therefore, manual memory management often leads to resource waste.
Java programs written in the language run in the virtual machine (JVM), which has the function of automatically recycling memory resources. However, this method often reduces the runtime efficiency, so the JVM tries to recycle resources as little as possible, which also causes the program to occupy a larger amount of memory resources.
Ownership is a novel concept for most developers, and it is a syntax mechanism designed by the Rust language for efficient memory usage. The ownership concept was born to make Rust more effectively analyze the usefulness of memory resources at the compilation stage to achieve memory management.
Ownership has the following three rules:
In Rust, each value has a variable, called its owner.
Only one owner can exist at a time.
When the owner is out of the program's running scope, the value will be deleted.
These three rules are the foundation of the ownership concept.
Next, we will introduce the concepts related to the ownership concept.
We use the following program to describe the concept of variable scope:
{ // Before the declaration, the variable s is invalid let s = "w3codebox"; // This is the available scope of the variable s } // The variable scope has ended, and the variable s is invalid
Variable scope is an attribute of a variable, representing the feasible domain of the variable, which is valid by default from the declaration of the variable to the end of the variable domain.
If we define a variable and assign it a value, the value of the variable exists in memory. This situation is very common. But if the length of the data we need to store is not certain (for example, a string input by the user), we cannot specify the length of the data when defining it, nor can we allocate a fixed-length memory space for data storage at the compilation stage. (Some say that allocating as much space as possible can solve the problem, but this method is not very polite). This requires a mechanism for the program to apply for memory use itself at runtime—the heap. All the "memory resources" mentioned in this chapter refer to the memory space occupied by the heap.
There is allocation and there is release; the program cannot keep occupying a certain memory resource. Therefore, the key factor to determine whether resources are wasted is whether resources are released in time.
We write the example program of the string in C language equivalently:
{ char *s = "w3codebox"; free(s); // Release s resource }
It is obvious that there is no call to the free function to release the resource of the string s in Rust (I know this is incorrect in C language, because "w3"codebox" is not in the heap, and it is assumed here that it is in()). Rust does not explicitly state the release steps because the Rust compiler automatically adds the step to call the release resource function when the variable scope ends.
This mechanism seems very simple: it is just a function call to release resources at the appropriate place, helping programmers. But this simple mechanism can effectively solve one of the most headache-inducing programming problems in history.
The main ways variables interact with data are move (Move) and clone (Clone):
Multiple variables can interact with the same data in different ways in Rust:
let x = 5; let y = x;
This program will assign the value 5 Bound to the variable x, then copy and assign the value of x to the variable y. Now there will be two values in the stack 5. In this case, the data is of the "basic data" type, which does not need to be stored in the heap, and the "move" method in the stack is a direct copy, which does not take longer time or more storage space. The "basic data" types include these:
All integer types, such as i32 , u32 , i64 And so on.
Boolean type bool, with values true or false.
All floating-point types, f32 And f64.
Character type char.
Tuples that only contain the above types of data.
But if the data being interacted with is already in the heap, it is a different situation:
let s1 = String::from("hello"); let s2 = s1;
The first step creates a String object with the value "hello". The "hello" can be considered as similar to data of uncertain length that needs to be stored in the heap.
The second step is slightly different (This is not completely true; it is only used for comparison and reference.):
As shown in the figure: there are two String objects in the stack, each with a pointer to the "hello" string in the heap. When assigning s2 is assigned, only the data in the stack is copied, and the string in the heap remains the original string.
as we mentioned before, when a variable goes out of scope, Rust automatically calls the resource release function and cleans up the heap memory of the variable. But when s1 And s2 are all released, the "hello" in the heap is released twice, which is not allowed by the system. To ensure safety, when assigning s2 is assigned, if s1 is invalid. That's right, when assigning s1 is assigned to s2 Later s1 can no longer be used. The following program is incorrect:
let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1); // Error! s1 is invalid
So in reality:
s1 is a name without substance.
Rust tries to minimize the runtime cost of programs, so by default, larger data is stored on the heap and data interaction is done through moves. However, if you need to simply copy data for use elsewhere, you can use the second form of data interaction—cloning.
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = "", s1, s2); }
Running result:
s1 = hello, s2 = hello
Here is a real copy of "hello" from the heap, so s1 And s2 Each binds a value, and is released as two resources when released.
Of course, cloning is only used when a copy is needed, as copying data takes more time.
This is the most complex case for variables.
How to safely handle ownership if a variable is passed as a parameter to another function?
The following program describes the working principle of the ownership mechanism in this case:
fn main() { let s = String::from("hello"); // s is declared as valid takes_ownership(s); // The value of s is passed as a parameter to the function // So it can be treated as if s has been moved, and it is invalid from here on let x = 5; // x is declared as valid makes_copy(x); // The value of x is passed as a parameter to the function // But x is a basic type, still valid // x can still be used here, but s cannot } // Function ends, x is invalid, then s. But s has been moved, so it does not need to be released fn takes_ownership(some_string: String) { // A String parameter some_string is passed, valid println!("{}", some_string); } // Function ends, parameter some_string is released here fn makes_copy(some_integer: i32) { // An i32 Parameter some_integer is passed, valid println!("{}", some_integer); } // Function ends, parameter some_integer is a basic type, no need to release
If a variable is passed as a parameter to a function, the effect is the same as moving it.
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return value to s1 let s2 = String::from("hello"); // s2 Declared as valid let s3 = takes_and_gives_back(s2); // s2 Moved as a parameter, s3 Ownership of the return value is obtained } // s3 Invalid is released, s2 Moved, s1 Invalid is released. fn gives_ownership() -> String { let some_string = String::from("hello"); // some_string is declared as valid return some_string; // some_string is moved out of the function as the return value } fn takes_and_gives_back(a_string: String) -> String { // The string 'a_string' is declared as valid a_string // a_string is moved out of the function as a return value }
The ownership of variables returned as function return values will be moved out of the function and returned to the calling function, rather than being invalidated directly.
Reference (Reference) is a C++ a concept that developers are more familiar with.
If you are familiar with the concept of pointers, you can think of it as a pointer.
Essentially, "reference" is an indirect access method to variables.
fn main() { let s1 = String::from("hello"); let s2 = &s1; println!("s1 is {}, s2 is {}", s1, s2); }
Running result:
s1 is hello, s2 is hello
The & operator can take the "reference" of the variable.
When a variable's value is referenced, the variable itself is not considered invalid. Because "reference" does not copy the value of the variable in the stack:
The reason for passing function parameters is the same:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
Running result:
The length of 'hello' is 5.
the reference will not obtain the ownership of the value.
the reference can only borrow the ownership of values.
the reference itself is also a type and has a value, which records the location of other values, but the reference does not have ownership of the pointed value:
fn main() { let s1 = String::from("hello"); let s2 = &s1; let s3 = s1; println!("{}", s)2); }
This program is incorrect: because s2 the borrowed s1 has already moved the ownership to s3, so s2 you will not be able to continue borrowing and using s1 the ownership. If you need to use s2 To use this value, you must re-borrow:
fn main() { let s1 = String::from("hello"); let mut s2 = &s1; let s3 = s2; s2 = &s3; // Re-borrow from s3 Borrowing ownership println!("{}", s)2); }
This program is correct.
Since the reference does not have ownership, even if it borrows ownership, it only has the right to use (this is the same as renting a house).
If you try to use the borrowed rights to modify the data, it will be blocked:
fn main() { let s1 = String::from("run"); let s2 = &s1; println!("{}", s)2); s2.push_str("oob"); // error, modification of the borrowed value is prohibited println!("{}", s)2); }
in this program s2 try to modify s1 the value is blocked, and the ownership of the borrowing cannot modify the owner's value.
Of course, there is also a mutable borrowing method, just like renting a house, if the property owner specifies that the landlord can modify the house structure, and the landlord also declares in the contract that you have the right to do so when renting, you can renovate the house again:
fn main() { let mut s1 = String::from("run"); // s1 is mutable let s2 = &mut s1; // s2 is a mutable reference s2.push_str("oob"); println!("{}", s)2); }
This program has no problem. We use &mut to modify the mutable reference type.
Compared to immutable references, mutable references do not allow multiple references, but immutable references can:
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2);
This program is incorrect because it has multiple mutable references to s.
The main reason for Rust's design of mutable references is to consider data access collisions in concurrent states, avoiding such things from happening at compile time.
Since one of the necessary conditions for a data access collision is that the data is written by at least one user and read or written by at least one other user at the same time, it is not allowed to have any other references when a value is mutable-referenced.
This is a concept with a different name, and if it is placed in a programming language with pointer concepts, it refers to those pointers that do not actually point to a real accessible data (note that it is not necessarily a null pointer, it may also be a resource that has been released). They are like objects hanging without a rope, so they are called "dangling references".
"Dangling Reference" is not allowed in Rust, and if it is, the compiler will find it.
Below is a typical example of a dangling reference:
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s }
It is obvious that with the end of the dangle function, the value of its local variables themselves were not used as return values and were released. However, its reference was returned, and the value it points to no longer exists, so it is not allowed to appear.