Take the first step with trust - learn how trust manages memory

Posted by almystersv on Thu, 09 Dec 2021 19:01:21 +0100

What is ownership

Rust contains an ownership system to manage memory At compile time, the ownership system checks a set of rules to ensure that the ownership feature allows your program to run without slowing down

To understand ownership, let's first look at Rust scope rules and mobile semantics

Scope rule

In Rust, like other languages, variables can be used in a defined scope In Rust, a scope is usually represented by {} The general scope includes the function body and if, else, match branches

Note: in Rust, "variable" is often referred to as "binding" Here, because "variables" are very invariant in Rust - they usually don't change because they don't change by default Instead, we usually think that the name is "bound" to the data, so it is named "binding" However, we use "variable" and "binding" alternately

We have a mascot variable, which is a string defined in the scope:

// `mascot` is not valid and cannot be used here, because it's not yet declared.
{
    let mascot = String::from("ferris");   // `mascot` is valid from this point forward.
    // do stuff with `mascot`.
}
// this scope is now over, so `mascot` is no longer valid and cannot be used.

If we try to use mascot outside its scope, we will get the same error as the following example

{
    let mascot = String::from("ferris");
}
println!("{}", mascot);
 error[E0425]: cannot find value `mascot` in this scope
     --> src/main.rs:5:20
      |
    5 |     println!("{}", mascot);
      |                    ^^^^^^ not found in this scope

A variable is available from its declaration point until the end of the scope

Ownership and dropping

Rust adds a scope with different ideas Whenever an object goes out of scope, it will be "discarded" Dropping a variable means releasing any resources that are bound to it For file variables, the file is finally closed For variables to allocate memory and them, this memory will be released

In Rust binding, he has something "associated" with them, and they will release it. When the binding is lost, it means "owning" those things

In the above example, the mascot variable has a string associated with it String has its own heap allocated memory and string characteristics At the end of the scope, the mascot is "discarded", the string itself is discarded, and finally the string in memory is released

{
    let mascot = String::from("ferris");
    // mascot dropped here. The string data memory will be freed here.
}

Mobile semantics

Sometimes we throw away at the end of the scope by associating a variable with something we don't want Instead, we want to transform the ownership of an object from one binding to another

The simplest example when declaring a new binding:

{
    let mascot = String::from("ferris");
    // transfer ownership of mascot to the variable ferris.
    let ferris = mascot;
    // ferris dropped here. The string data memory will be freed here.
}

The key is to understand that once ownership is transferred, the old variables are no longer available In our previous example, after we transfer the ownership of String from mascot to ferris, we can no longer use mascot variables

In Rust, "transfer of ownership" is known as "movement" In other words, in the above example, the String value has been moved from mascot to ferris

If we use mascot, the compiler cannot compile the code after the String has been moved from mascot to ferris:

{
    let mascot = String::from("ferris");
    let ferris = mascot;
    println!("{}", mascot) // We'll try to use mascot after we've moved ownership of the string data from mascot to ferris.
}
error[E0382]: borrow of moved value: `mascot`
 --> src/main.rs:4:20
  |
2 |     let mascot = String::from("ferris");
  |         ------ move occurs because `mascot` has type `String`, which does not implement the `Copy` trait
3 |     let ferris = mascot;
  |                  ------ value moved here
4 |     println!("{}", mascot);
  |                    ^^^^^^ value borrowed here after move

This is the famous "use after move" compilation error

Important: in Rust, only one can always have data for this period of time

Ownership in function

Let's look at an example of a string transferred to a function as an argument Pass something as an argument to the function and move it to the function

fn process(input: String) {}

fn caller() {
    let s = String::from("Hello, world!");
    process(s); // Ownership of the string in `s` moved into `process`
    process(s); // Error! ownership already moved.
}

The compiler complains that the value s has moved

error[E0382]: use of moved value: `s`
     --> src/main.rs:6:13
      |
    4 |     let s = String::from("Hello, world!");
      |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
    5 |     process(s); // Transfers ownership of `s` to `process`
      |             - value moved here
    6 |     process(s); // Error! ownership already transferred.
      |             ^ value used here after move

As you can see in the above code, first call process to change the ownership of the variable s The compiler tracks ownership, so an error occurs in the next call to process After the resource is moved, the previous owner is no longer available

This mode greatly affects the Rust coding method Its core is to ensure memory security, proposed by Rust

In other languages, the String value of the s variable can be implicitly copied before it is passed into our function But this operation will not happen in Rust

In Rust, ownership transfer (that is, move) is the default behavior

Copy instead of move

You may notice the compiler error message above, Copy trait is mentioned We haven't discussed trait at present, but the value implements Copy trait, which is copied instead of being moved
Let's look at the value that implements Copy trait u32 The following code reflects that we violated the above code, but the compiler did not have a problem

fn process(input: u32) {}

fn caller() {
    let n = 1u32;
    process(n); // Ownership of the number in `n` copied into `process`
    process(n); // `n` can be used again because it wasn't moved, it was copied.
}

Simple types are like numeric copy types They implemented Copytrait, which means that their copying is equivalent to moving The same action occurs for simple types Copying values is low consumption, so it makes sense for those values to be copied Copying string or vector or other complex types is very expensive, so they do not implement Copytrait and use mobile instead

The Copy type does not implement Copy

One way to handle errors, we see the above display by copying the type before it moves: using clone. In Rust A call Clone copies memory and generates new values The new value is moved, which means that the old value can still be used

fn process(s: String) {}

fn main() {
    let s = String::from("Hello, world!");
    process(s.clone()); // Passing another value, cloned from `s`.
    process(s); // s was never moved and so it can still be used.
}

This method can be used, but it can slow down your code when clone is a full copy of data for each call This method usually involves memory allocation or other expensive operations We can avoid these overhead if we can "borrow" values by reference We learn to quote in the next unit

Learning borrowing

https://docs.microsoft.com/en-us/learn/modules/rust-memory-management/2-learn-about-borrowing

Topics: Back-end Rust