Rust ownership
Computer programs must manage the memory resources they use at run time.
Most programming languages have the ability to manage memory:
Languages such as CUniverse + mainly manage memory manually, and developers need to manually apply for and release memory resources. However, in order to improve the development efficiency, as long as it does not affect the implementation of program functions, many developers do not have the habit of releasing memory in time. So the way of managing memory manually often results in a waste of resources.
The program written in Java language runs in the virtual machine (JVM), and JVM has the function of automatically reclaiming memory resources. But this approach often reduces runtime efficiency, so JVM will recycle as few resources as possible, which will also make the program take up more memory resources.
Ownership is a novel concept for most developers, and it is the syntax mechanism designed by the Rust language to use memory efficiently. The concept of ownership is developed to enable Rust to analyze the usefulness of memory resources more effectively during the compilation phase in order to achieve memory management.
Ownership rule
There are three rules for ownership:
Each value in Rust has a variable called its owner.
There can be only one owner at a time.
This value is deleted when the owner is not in the scope of the program.
These three rules are the basis of the concept of ownership.
Next, we will introduce concepts related to the concept of ownership.
Variable range
We use the following program to describe the concept of variable scope:
{
//Invalid variable s before declaration
Lets="runoob";
//Here is the available range of variable s
}
//Variable range has ended, variable s is invalid
A variable scope is an attribute of a variable that represents the feasible domain of the variable and is valid by default from declaring the variable to the end of the variable’s domain.
Memory and allocation
If we define a variable and assign it a value, the value of that variable exists in memory. This situation is very common. But if the length of the data we need to store is uncertain (such as a string entered by the user), we cannot specify the length of the data at the time of definition, and we will not be able to make the program allocate a fixed length of memory space for data storage during the compilation phase. Some people say that allocating as much space as possible can solve the problem, but this method is very uncivilized. This needs to provide a mechanism for the program to apply for memory while the program is running-heap. All the “memory resources” in this chapter refer to the memory space occupied by the heap.
Where there is allocation, there is release, and programs cannot occupy a certain memory resource all the time. Therefore, the key factor to determine whether resources are wasted is whether resources are released in a timely manner.
Let’s write the sample string program in C language equivalent:
{
char *s = strdup("runoob");
free(s); // Release s resources
}
Obviously, there is no call in Rust free
function to release the resources of the string s (I know this is not the correct way to write in C because “runoob” is not in the heap, so let’s assume it is). The reason why Rust does not explicitly release the step is that when the variable range ends, the Rust compiler automatically adds the step of calling the release resource function.
This mechanism seems simple: it just helps programmers add a function call that frees resources in the right place. But this simple mechanism can effectively solve one of the most troublesome programming problems in history.
The way variables interact with data
There are two main ways for variables to interact with data: Move and Clone:
move
Multiple variables can interact with the same data in different ways in Rust:
let x = 5;
let y = x;
This program binds the value 5 to the variable x, then copies the value of xand assigns it to the variable y. There will now be two values of 5 in the stack. The data in this case is of the “basic data” type and does not needto be stored in the heap, and the “move” mode of the data in the stack iscopied directly, which does not take longer or more storage space. The basic data types are as follows:
All integer types, such as
i32
、u32
、i64
, etc.Boolean type
bool
with a value oftrue
orfalse
.All floating point types
f32
andf64
.Character type
char
.A tuple that contains only the above types of data.
But if the interactive data is in the heap, it is a different situation:
let s1 = String::from("hello");
let s2 = s1;
The first step is to produce a String
object with a value of “hello”.Where “hello” can be thought of as data of uncertain length, which needs to be stored in the heap.
The situation in the second step is slightly different (this is not entirelytrue, it is only for comparative reference):
As shown in the figure: two String
object in the stack, each String
objects have a pointer to the “hello” string in the heap. In giving s2
when assigning, only the data in the stack is copied, and thestring in the heap is still the original string.
As we said earlier, when a variable is out of range, Rust automatically calls the free resource function and cleans up the variable’s heap memory. But s1
and s2
if both are released, the “hello” in the heap areais released twice, which is not allowed by the system. In order to ensure safety, give s2
when assigning values s1
is no longer effective.That’s right. I’m putting s1
value assigned to the s2
later, s1
will no longer be available for use. The following procedure is wrong:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // Error! S1 has expired
So the actual situation is:
S1 exists in name only.
Clone
Rust reduces the cost of running the program as much as possible, so by default, longer data is stored in the heap and the data is exchanged in a mobile manner. But if you need to simply make a copy of the data for other use, you can use the second way to interact with the data-cloning.
Example
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 the “hello” in the heap, so s1
and s2
Each is bound to a value, and when released, it is treated as two resources.
Of course, cloning is only used when replication is needed, since it takes more time to replicate data.
Ownership mechanism involving functions
This is the most complicated case for variables.
How do you safely handle ownership if you pass a variable to another function as an argument to the function?
The following program describes how the ownership mechanism works in this case:
Example
Fn main(){
Lets=String:: from ("hello");
//S declared valid
Take_ Ownership (s);
//The value of s is passed into the function as a parameter
//So it can be considered that s has been moved and is no longer valid from here on
Let x=5;
//X declared valid
Make_ Copy (x);
//The value of x is passed into the function as a parameter
//But x is the basic type and still valid
//You can still use x here, but not s
}//Function ended, x invalid, followed by s. However, s has been moved, so it does not need to be released
Fn takes_ Ownership (some_string: String){
//A String parameter some_ String passed in, valid
Println! ({} ", some_string);
}//Function ended, parameter some_ String is released here
Fn makes_ Copy (some_integer: i32){
//An i32 parameter some_ Incoming integer, valid
Println! ({} ", some_integer);
}//Function ended, parameter some_ Integer is a basic type and does not need to be released
If you pass a variable into a function as an argument, it has the same effect as moving.
Ownership mechanism of function return value
Example
Fn main(){
Let s1=gifts_ Ownership();
//Gives_ Ownership moves its return value to s1
Let s2=String:: from ("hello");
//S2 declared valid
Let s3=takes_ And_ Gives_ Back (s2);
//S2 is moved as a parameter, and s3 obtains ownership of the return value
}//s3 is invalid and released, s2 is moved, and s1 is invalid and released
Fn gifts_ Ownership() ->String{
Let some_ String=String:: from ("hello");
//Some_ String declared valid
Return some_ String;
//Some_ String is moved out of the function as a return value
}
Fn takes_ And_ Gives_ Back (a_string: String) ->String{
//A_ String declared valid
A_ String//a_ String is used as a return value to move out of the function
}
The ownership of the variable that is treated as the return value of the function will be moved out of the function and returned to the place where the function is called, rather than being invalidly released directly.
Citation and lease
Reference is a concept that C++ developers are familiar with.
If you are familiar with the concept of pointer, you can think of it as a pointer.
In essence, “reference” is the indirect access to variables.
Example
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 a reference to a variable.
When the value of a variable is referenced, the variable itself is not considered invalid. Because reference does not copy the value of the variable in the stack:
Function arguments are passed in the same way:
Example
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 does not take ownership of the value.
Reference can only lease (Borrow) the ownership of values.
The reference itself is also a type and has a value that records the location of other values, but the reference does not have ownership of the specified value:
Example
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
let s3 = s1;
println!("{}", s2);
}
This procedure is incorrect: because s2
leased s1
ownership has been moved to s3
, so s2
will not be able to continue to lease s1
ownership of. If you need to use the s2
with this value, you must re-lease:
Example
fn main() {
let s1 = String::from("hello");
let mut s2 = &s1;
let s3 = s1;
s2 = &s3; // Renting ownership from s3
println!("{}", s2);
}
This procedure is correct.
Since the reference does not have ownership, even if it rents ownership, it only has the right to use it (which is the same as renting a house).
Attempts to modify data using leased rights will be blocked:
Example
fn main() {
let s1 = String::from("run");
let s2 = &s1;
println!("{}", s2);
s2.push_str("oob"); // Error, it is prohibited to modify the rental value
println!("{}", s2);
}
In this program, s2
try to modify, s1
is blocked and the ownership of the lease cannot modify the owner’s value.
Of course, there is also a variable lease, just like you rent a house, if the property stipulates that the owner can modify the structure of the house, and the owner also declares this right in the contract when renting, you can redecorate the house:
Example
fn main() {
let mut s1 = String::from("run");
// S1 is variable
let s2 = &mut s1;
// S2 is a variable reference
s2.push_str("oob");
println!("{}", s2);
}
There will be no problem with this procedure. We use &mut
modifies a variable reference type.
Mutable references do not allow multiple references compared to immutable references except for permissions, but immutable references can:
Example
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
This program is incorrect because s is referenced by multiple variables.
Rust’s design of variable references is mainly based on the consideration ofdata access collisions in the concurrent state, which is avoided during thecompilation phase.
Since one of the necessary conditions for data access collisions is that thedata 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 be referenced again when avalue is variably referenced.
Dangling References
This is a concept with a different name, and if you put it in a programming language with a pointer concept, it refers to a pointer that doesn’t actually point to data that can actually be accessed (note, it’s not necessarily a null pointer, it could also be a resource that has been released). They are like ropes that lose hanging objects, so they are called”hanging references”.
A “dangling reference” is not allowed in the Rust language, and if so, thecompiler will find it.
Here is a typical case of overhanging:
Example
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Obviously, along with dangle
at the end of a function, the value of itslocal variable itself is not treated as a return value and is released. However, its reference is returned, and the value pointed to by this reference can no longer be determined, so it is not allowed.