One of Rust’s core features (ownership)

Table of contents

1. What is ownership?

1.1 Ownership Rules

 1.2 Variable scope

1.3 String type

1.4 Memory and allocation

How variables interact with data (1): Move

How variables interact with data (2): cloning

Data only on the stack: copy

1.5 Ownership and functions

1.6 Return value and scope


1. What is ownership?

Ownership (system) is Rust’s most distinctive feature and has profound implications for the rest of the language. It makes Rust memory safe without the need for a garbage collector, so it's important to understand how ownership works in Rust.

All programs must manage how they use your computer's memory while they run. Some languages ​​have a garbage collection mechanism that regularly looks for unused memory while the program is running; in other languages, programmers must allocate and release memory themselves. Rust has chosen the third approach: managing memory through an ownership system, which the compiler will check according to a series of rules at compile time. If any of these rules are violated, the program will not compile. While running, no features of the ownership system will slow down the program.

Because ownership is a new concept to many programmers, it takes some getting used to. The good news is that as you become more experienced with Rust and the rules of the ownership system, writing safe and efficient code becomes more natural to you.

1.1 Ownership Rules

First, let's look at the rules of ownership. As we illustrate through examples, keep these rules in mind:

  1. Every value in Rust has an owner (owner< a i=4>).
  2. A value has exactly one owner at any time.
  3. When the owner (variable) goes out of scope, this value will be discarded.

 1.2 Variable scope

In our first example of ownership, we look at the scope of some variables ( scope). Scope is the range within a program within which an item is valid. Suppose there is a variable like this:

let a = 12345

The variable a is bound to a string literal, which is hard-coded into the program code. This variable is valid from the point of declaration until the end of the current scope .

fn main() {                // 作用域开始处
   let a = 12345;          // 此时,a才开始分配存储空间  
}                          // 作用域开始处

In fact, the relationship between whether a variable is valid and its scope is similar to that in other programming languages.

1.3 String type

We have already seen string literals, which are string values ​​that are hardcoded into a program. String literals are convenient, but they are not suitable for every use case of text. One reason is that they are immutable. Another reason is that not all string values ​​can be known while writing the code: for example, what if you want to take user input and store it? For this purpose, Rust has a second string type, String. This type manages data allocated on the heap, so it can store text of unknown size at compile time. You can use the from function to create String based on a string literal, as follows:

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

The two colons :: are operators that allow specific from functions to be named in the String type space (namespace), without using a name like string_from .

CanModify this type of string mutability:

fn main() {                
    let mut s = String::from("hello ");
    s.push('w');
    s.push_str("orld");
    println!("{s}")
}                          

So what’s the difference here? Why is String mutable but not literal values? The difference lies in the way the two types handle memory.

1.4 Memory and allocation

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. But these features only benefit from the immutability of string literals. Unfortunately, we cannot put a block of memory into the binary file for every text whose size is unknown at compile time, and its size may also change as the program runs.

For the String type, in order to support a variable, growable text fragment, a memory of unknown size at compile time needs to be allocated on the heap to store the content. This means:

  • Memory must be requested from the memory allocator at runtime.
  • needs a way to return the memory to the allocator when we are done with String .

The first part is done by us: when String::from is called, its implementation (implementation) asks its required memory. This is very common in programming languages.

However, the second part is implemented differently. In the presence of garbage collector (garbage collector, GC), GC records and clears memory that is no longer used, and we don't need to care about it. In most languages ​​without GC, it is our responsibility to identify memory that is no longer in use and call code to release it explicitly, just like when requesting memory. Historically speaking, handling memory collection correctly has been a difficult programming problem. If you forget to recycle, memory will be wasted. If recycled prematurely, invalid variables will appear. If recycled repeatedly, this is also a bug. We need to match exactly one allocate with one free.

Rust adopts a different strategy: memory is automatically released after the variable that owns it goes out of scope.

This is a natural place to return the memory needed by String to the allocator: when the variable goes out of scope, Rust calls a special function for us. This function is called drop, where the author of String can place the code to release the memory. Rust automatically calls  at the end of } . drop

Note: In C++, this model of item releasing resources at the end of its life cycle is sometimes called Resource acquisition is initialization ( Resource Acquisition Is Initialization (RAII)). If you have used RAII mode, you should be familiar with Rust's drop function.

How variables interact with data (1): Move

fn main() {
    let x = 5;
    let y = x;
}

The two simple variables defined will be stored directly on the stack because their lengths are fixed.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

When s1 and s2 are stored, an area will be opened in the heap for storage. s1 and s2 use pointers to point to the storage locations in the heap, which can avoid multiple allocations in the heap. storage space, if you have heard the term shallow copy(shallow copy< a i=4>) and deep copy (deep copy), then Copying the pointer, length, and capacity without copying the data may sound like a shallow copy. But because Rust also invalidates the first variable, this operation is called move(move . If you print s1, the following error will appear: to  s1 being moved ), instead of being called a shallow copy. The above example can be interpreted as s2

How variables interact with data (2): cloning

 If we  do  need a deep copy of the data on the heap in String , not just on the stack For data, you can use a general function called clone .

Here is an example:

fn main() {                
    let s1 = String::from("hello ");
    let s2 = s1.clone();
    println!("{s1} {s2}")
}       

Print the following to see the results:

Data only on the stack: copy

fn main() {
    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);
}

 Rust has a special annotation called Copy trait that can be used on types stored on the stack like integers. If a type implements the Copy trait, then an old variable is still available after assigning it to another variable.

So which types implement Copy Trait? There are the following types:

  • All integer types, such as u32.
  • Boolean type, bool, its values ​​are true and false.
  • All floating point number types, such as f64.
  • Character type,char.
  • tuple, if and only if the types it contains also implement Copy . For example, (i32, i32) implements Copy, but (i32, String) does not.

1.5 Ownership and functions

Passing a value to a function works similarly to assigning a value to a variable. Values ​​passed to functions may be moved or copied, just like assignment statements.

An example:

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,
                                    // 所以在后面可继续使用 x

} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 没有特殊之处

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。
  // 占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。没有特殊之处

You can find that when the value is passed to the function, the data in the heap willremove the current scope, for example, s passes the takes_ownership method Here, the value of s in the current scope is released, and the value in the stack is passed to the function, and its corresponding operation is copying. For example, after x=5 is passed to the function, it can still be accessed in the current scope.

1.6 Return value and scope

Return values ​​can also transfer ownership. Here is an example:

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值
                                        // 转移给 s1

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                        // takes_and_gives_back 中,
                                        // 它也将返回值移给 s3
} // 这里,s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 离开作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 会将
                                             // 返回值移动给
                                             // 调用它的函数

    let some_string = String::from("yours"); // some_string 进入作用域。

    some_string                              // 返回 some_string 
                                             // 并移出给调用的函数
                                             // 
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
                                                      // 

    a_string  // 返回 a_string 并移出给调用的函数
}

This is the same as the moving rule above. Move the value when assigning it to another variable. When a variable holding a data value in the heap goes out of scope, its value will be cleared via drop unless the data is moved to another variable.

Above we have obtained everything through a variable, so assigning values ​​back and forth will be a bit cumbersome. In this case, we can use tuples to return multiple values.

fn main() {
    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(); // len() 返回字符串的长度

    (s, length)
}

But this is a bit formalistic, and this scenario should be very common. Fortunately, Rust provides a function for using values ​​without taking ownership, called reference( references), let’s take a look at what references are later?

Guess you like

Origin blog.csdn.net/u014388408/article/details/134141656