Persistence is the only road to truth.
One of Rust’s core functions, which other languages don’t have, is ownership.
All running programs must manage the way they use the computer’s memory. Some languages have garbage collection mechanisms, such as JS and Java virtual machines, which constantly find unused memory while the program is running. In other languages, the programmer must allocate and free memory himself, such as c++.
Rust takes a third approach: managing memory through an ownership system that the compiler checks against a set of rules at compile time. None of the features of the ownership system slows down the program at run time.
The semantic tree in the figure above is mainly intended to express the following meanings:
- Ownership is a whole concept systematically composed of several concepts.
- Let binding, what binding? Variable + scope + data (memory).
- Move, Lifetime, and RAII are all scope-specific, so understanding them requires understanding scope
Heap, stack
Many programming languages don’t really need to consider stacks, but rust system-level programming languages do.
A: the stack
Stack (LIFO) last in first out storage deconstruction. Example: Folding dishes.
- Adding data is calledInto the stack(
pushing onto the stack
). - And moving out data is calledOut of the stack(
popping off the stack
). - All data in the stack must occupy a known and fixed size. Data whose size is unknown or may change at compile time is stored on the heap instead.
B: the heap
The heap requests a certain amount of space before it dumps data. The operating system finds a large enough empty space somewhere in the heap, marks it as used, and returns a pointer to the address of that location. This process is called allocating memory on the heap, and is sometimes referred to simply as “allocating”.
Example: Sit down to eat in a restaurant. When you enter, you say how many people there are, and the restaurant staff will find an empty table big enough to show you. If someone is late, they can also ask to find out where you are sitting.
C: Compare the two
-
Pushing data onto the stack is not considered allocation. Because the size of the pointer is known and fixed, you can store the pointer on the stack, but the pointer must be accessed when you need actual data.
-
Pushing is faster than allocating memory on the heap because the operating system does not have to search for memory space to store new data; It’s always at the top of the stack.
-
When a function is called, the values passed to the function (including possible Pointers to data on the heap) and the local variables of the function are pushed onto the stack. When the function ends, these values are moved off the stack.
Summary: Ownership systems deal with tracking which parts of code are using which data on the heap, minimizing the amount of duplicate data on the heap, and cleaning up unused data on the heap to make sure it doesn’t run out of space.
Ownership rules:
Rust
Each value has a variable called its owner.- Values have one and only one owner.
- This value is discarded when the owner (variable) leaves scope.
Type String
There are two common String types in Rust: STR and String. STR is the core Rust language type, the String Slice we’ve been talking about in this chapter, often in the form of a reference (& STR).
Any string constant enclosed in double quotes has the type property &str as a whole:
let s = "hello";
Copy the code
The String type is a data type provided by the Rust standard common library. It has more functionality — it supports practical operations such as appending and emptying strings. String and STR also have a capacity attribute in addition to a character start position attribute and a String length attribute.
Both String and STR support slicing, which results in &str data.
Note: Slice results must be of reference type. :
let slice = &s[0.3];
// There is a quick way to convert String to &str:
let s1:String = String::from("hello");
let s2:&str= &s1[..] ;/ / on the contrary,
let s3:String = "qiuyanlong".to_string();
Copy the code
The core difference between a String argument and a String is how the two types deal with memory.
// A formal declaration of a string argument
let s = "hello";
println!("{}", s);
Copy the code
The argument form declares that string values are hard-coded into the program. String literals are handy, but they are not suitable for every situation in which text is used. There are two main disadvantages:
- Immutable.
- Not all string values can be known at code time, for example, if you want to take user input and store it.
For this reason, Rust has a second String type, String. This type is allocated to the heap, so it can store text strings whose size is unknown at compile time.
fn second_string() {
//:: is the operator that allows specific 'from' functions to be placed in a namespace of type String
let mut s = String::from("hello");
s.push_str(", world");
println!("{}", s);
}
Copy the code
Summary: So what’s the difference here? Why are strings mutable and literals not? The difference lies in how the two types treat memory
In the case of string literals, we know their contents at compile time, so the text is hardcoded directly into the final executable. This makes string literals fast and efficient. However, these features only benefit from the immutability of string literals.
Unfortunately, you can’t put a chunk of memory into a binary file for every piece of text whose size is unknown at compile time, and its size can change as the program runs.
Memory and allocation
In order to support a mutable, growable text fragment, a chunk of memory on the heap of an unknown size at compile time is allocated to hold the content. This means:
- Memory must be requested from the operating system at runtime. When calling
String::from
, its implementation (implementation) request the memory it needs. - We need one when we’re done
String
Method of returning memory to the operating system. Where there isThe garbage collection(garbage collector.GC), GC records and cleans up memory that is no longer used, and we don’t need to care about it.
But Rust takes a different strategy: When a variable that owns it leaves scope, it is automatically released, and Rust calls the drop method.
fn main() {{let s = String::from("hello"); // From this point on, s is valid
/ / s
} // This scope is finished,
} // s is no longer valid
Copy the code
This pattern has a profound impact on the way Rust code is written. It seems simple now, but the code can behave unpredictably in more complex scenarios, such as when there are multiple variables using memory allocated on the heap. Now let’s explore some of these scenarios.
Variables interact with data in the following ways: 1: Move
Mobile is the transfer of ownership. When will the ownership transfer? When a variable is moved, of course, from one place to another. You can also assume that when a variable is switched to another scope, its binding in the current scope will be unbound and its data will be rebound in the other scope.
But for types that implement Copy traits, when a move occurs, they can replace it with a Copy while retaining ownership. For example, basic numeric types in Rust implement Copy traits by default, as shown in the following example:
let num = 42; let num2 = num; println! (" {:? }", num);Copy the code
At this point, we print num, and the compiler does not report an error. Num has already moved, but since the numeric type is the default implementation of Copy Trait, it moves a Copy of itself and its ownership remains. Note, however, that Rust does not allow Copy traits to be used by types that implement the Drop trait itself or any part of it.
When a move occurs, the variable whose ownership was transferred is released.
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // The information is borrowed here after move
Copy the code
When a variable is out of scope, Rust automatically calls the drop function and cleans up the heap memory for the variable. But the figure above shows two data Pointers pointing to the same location. Here’s a problem: when S2 and S1 go out of scope, they both try to free the same memory. This is an error called double free, and one of the memory security bugs mentioned earlier. Freeing (the same) memory twice causes memory contamination, which can lead to potential security vulnerabilities
To ensure memory security, there is another detail to the handling of Rust in this scenario that is worth noting. Instead of trying to copy allocated memory, Rust thinks s1 is no longer valid, so Rust doesn’t need to clean anything up after S1 goes out of scope.
Shallow copy and deep copy, so copying Pointers, length, and capacity without copying data may sound like shallow copy. But because Rust also invalidates the first variable, this operation is called a move, not a shallow copy.
Variables interact with data in the following ways: 2: clone
If we really need to make a deep copy of the String data on the heap, not just on the stack, we can use a generic function called clone.
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
Copy the code
Only data on stack: 3: copy
fn main() {
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
}
Copy the code
Types such as integers, whose size is known at compile time, are stored on the stack as a whole, so copying their actual value is fast. This means that there is no reason to invalidate x after creating variable y.
Special annotations for Copy traits can be used on stack-stored types such as integers. Types have Copy traits, where an old variable remains available even after it is assigned to another variable. The following are the parts that implement this annotation.
- Any combination of simple scalar values can be
Copy
The type that does not need to allocate memory or some form of resource isCopy
的 - All integer types, such as
u32
. - Boolean type,
bool
And its value is zerotrue
和false
. - All floating point types, such as
f64
. - Character type,
char
. - Tuples, if and only if they contain both types
Copy
From time to time. For instance,(i32, i32)
It’s Copy, but(i32, String)
It is not.
Ownership and function
fn main() {
let s = String::from("hello"); // s enters scope
takes_ownership(s); // move the value of s to the function...
/ /... So it's no longer valid up here
let x = 5; // x goes into scope
makes_copy(x); // x should move in the function,
// I32 is Copy, so we can continue to use x later
} // here, x moves out of scope first, then s. But since the value of s has been removed,
// There will be no special operations
fn takes_ownership(some_string: String) { // some_string enters scope
println!("{}", some_string);
} // Here, some_string moves out of scope and calls the 'drop' method. The occupied memory is released. Procedure
fn makes_copy(some_integer: i32) { // some_integer enters the scope
println!("{}", some_integer);
} // here, some_INTEGER moves out of scope. There will be no special operations
Copy the code
Functions that return values can transfer ownership, but this part of rust is implemented using references.
fn main() {
// let (x, y): (usize, isize) = (1, 2);
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) - > (String.usize) {
let length = s.len();
// Return a tuple,
(s, length);
}
Copy the code
Variable ownership always follows the same pattern: move a value when assigning it to another variable. When a variable holding data values in the heap leaves scope, its value is cleared by DROP, unless the data is moved to another variable.
Quote and borrow
Sometimes we don’t want to transfer ownership of a variable. For example, if I write a function that simply inserts a fixed value into an array:
fn push(vec: &mut Vec<u32>) { vec.push(1); } fn main(){ let mut vec = vec! [0, 1, 3, 5]; push(&mut vec); println! (" {:? }", vec); }Copy the code
At this point, we pass the array vec to push, so we don’t want to transfer ownership, so we just pass a mutable reference &mut vec, because we need to change vec, so push gets a mutable borrow of the vec variable, let’s change it. After the push function is modified, it will return the ownership to vec, and then println! Function can be printed using VEC smoothly.
References are handy, but can cause security issues if abused, such as dangling Pointers. Look at the following example:
let r; { let a = 1; r = &a; } println! ("{}", r);Copy the code
In the code above, a is released when it leaves scope, but r still holds a borrow of A. The borrow checker in the compiler tells you that a does not live long enough. Translation: A didn’t live long enough. This means that the lifetime of A is too short to lend to R, otherwise & A would point to an object that once existed but now no longer exists. This is the dangling pointer, also known as the wild pointer.
Reference is a concept familiar to C++ developers. If you’re familiar with the concept of a pointer, you can think of it as a pointer. Essentially a “reference” is an indirect access to a variable.
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
println!("s1 is {}, s2 is {}", s1, s2); // hello hello
}
Copy the code
The & operator takes a “reference” to a 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 on the stack.
fn main() {
let s1 = String::from("hello");
The &s1 syntax lets us create a reference to the value s1 without owning it.
// Since it does not own the value, the value it points to is not discarded when the reference leaves scope.
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
// Function signatures use '&' to indicate that the type of the argument 's' is a reference
fn calculate_length(s: &String) - >usize {
// s is a reference to String s.len()
}
// here, s is out of scope. But since it does not own the reference value, // nothing happens
Copy the code
These ampersands are references, and they allow you to use a value without taking ownership of it.
- References do not take ownership of values.
- References can only Borrow the ownership of values.
- A reference is itself a type and has a value that records the location of other values, but a reference does not have ownership of the referenced value
- Disallow modification of rental values, except
mut
Embellished. So mutable references.
Mutable reference:
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("> > {}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Copy the code
Restriction: There is only one mutable reference for a particular data in a particular scope. The advantage of this restriction is that Rust can avoid data contention at compile time. ** Data races are similar to race conditions.
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // Second mutable borrow occurs here
println!("{}, {}", r1, r2);
Copy the code
Use:
We call borrowing as a function parameter, just as in real life, if someone owns something, you can borrow it from them. When you’re done with it, you have to give it back.
Quote summary:,
-
There can be either one mutable reference or multiple immutable references at any given time.
-
A reference must always be valid
The scope of a reference begins where it is declared and continues until it is last used. For example, because an immutable reference was last used before a mutable reference was declared, the following code could be compiled:
let mut s = String::from("hello");
let r1 = &s; / / no problem
let r2 = &s; / / no problem
let r3 = &mut s; // Big problems will be reported, because the following print uses R1 and R2 again
println!("{}, {}, and {}", r1, r2, r3);
Copy the code
Dangling References
In a pointer language, it is very easy to incorrectly generate a dangling pointer by releasing memory while retaining the pointer to it. A dangling pointer refers to memory that has already been allocated to another holder.
In Rust, by contrast, the compiler ensures that references never become dangling: When you have a reference to some data, the compiler ensures that the data does not go out of scope before it is referenced.
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() - > &String { Dangle returns a reference to a string
let s = String::from("hello"); // s is a new string
&s Return a reference to the string s
// Return s;
} // where s leaves scope and is discarded. Its memory is freed.
/ / danger!
Copy the code
Slice slice
Another data type that has no ownership is slice. Slice allows you to refer to a contiguous sequence of elements in a collection without referring to the entire collection.
A Slice is a partial reference to a data value.
There are two common String types in Rust: STR and String.
STR is the core Rust language type, which is the String Slice we have been talking about in this chapter. It often appears in the form of a quotation & STR. Any String constant enclosed in double quotation marks has an overall type property of & STR.
let s = "hello";
Copy the code
The string type in Rust essentially records the starting position of a character in memory and its length.
fn main() {
let s = String::from("broadcast");
let part1 = &s[0.5]; // open a structure with the front closed and the back open
let part2 = &s[5.9];
println!("{} = {} + {}", s, part1, part2);
}
Copy the code
Some common ellipses:
. Y is equivalent to 0.. y x.. Equivalent to position x to end of data.. This is equivalent to position 0 to endCopy the code
Slicing referenced strings is prohibited from changing their value because they are only partial references, and the slicing result must be of reference type, but the developer must make this clear himself:
fn main() {
let s = String::from("broadcast");
let slice = &s[0.3];
s.push_str('aa'); // Error exception
println!("slice>>> {}", slice);
}
Copy the code
Non-string slice:
In addition to strings, some other linear data structures also support slicing, such as arrays:
fn main() {
let arr = [1.2.3.4.5.6];
let part = &arr[..3];
for i in part.iter() {
println!("> > > {}", i); }}Copy the code
Refer to the article
- Rust.bootcss.com/ch10-03-lif…
- Zhuanlan.zhihu.com/p/27571264?…