Borrowing and lifetime in Rust

I present to you the translation of the article “Rust Borrow and Lifetimes” from the blog of Arthur Liao, an engineer at Yahoo!

Rust is a new programming language that has been under active development since version 1.0 . I can write another blog about Rust and why it is cool, but today I will focus on its borrowing system and lifetime, which confuses many newcomers, including myself. This post assumes you have a basic understanding of Rust. If not, you can first read the Guide itself and the Guide to Signposts .

Resource Ownership and Borrowing


Rust provides memory security without garbage collection by using a sophisticated borrowing system. There is at least one owner for any resource that is releasing its resources. You can create new binders to access a resource using & or & mut, which are called borrow and mutable borrow. The compiler monitors the proper behavior of all owners and borrowers.

Copy and move


Before moving on to the borrowing system, we need to know how the copy and move methods are processed in Rust. This answer from StackOverflow just needs to be read. In general, they are called in assignments and functions:

1. If the value is copied (with the participation of only primitive types, without the participation of resources, for example, memory or file processing), the compiler copies by default.
2. Otherwise, the compiler moves (transfers) ownership and invalidates the original binding.

In short, pod (read old data) => copy, non-pod (linear types) => move.

There are a few additional notes for reference:

* Rust copy method is similar to C. Each usage by value is a byte copy (shadow memcpy copy method) instead of semantic copying or cloning.
* To make the pod structure uncopyable, you can use the NoCopy marker field or implement the Drop trait.

After moving, ownership is transferred to the next owner.

Resource Release


In Rust, any object is freed as soon as its ownership disappears, for example, when:

1. The owner is outside the scope or
2. The ownership of the binding changes (thereby, the original binding becomes void)

Privileges and restrictions of the owner (borrower) and borrower (borrower)


This section is based on the Rust Handbook with reference to the copy and move methods in terms of privileges.

The owner has some privileges. And it can:

1. Control the resource allocation;
2. Borrow the resource unchanged (multiple borrowings) or changeable (exclusive); and
3. Transfer ownership (with relocation).

The owner also has some limitations:

1. In the process of borrowing, the owner cannot (a) change the resource or (b) borrow it in an altered form.
2. In the process of variable borrowing, the owner cannot (a) have access to the resource or (b) borrow it.

The borrower also has some privileges. In addition to gaining access or changing a borrowed resource, the borrower can also share with another borrower:

1. The borrower can distribute (copy) the pointer an immutable borrow (immutable borrow)
2. A variable borrower can transfer (move) a variable borrowing. (Note that the mutable reference has been moved).

Code examples


Enough talk. Let's take a look at some code (you can run Rust code at play.rust-lang.org). In all of the following examples, we use “struct Foo,” a Foo structure that is not copyable because it contains a packed (dynamically allocated) value. The use of uncopyable resources limits the possibilities of operations, which is a good idea at the study stage.

For each sample code, a “region chart” is also given to illustrate the areas of the owner, borrowers, etc. The curly brackets in the title bar coincide with the curly braces in the code itself.

The owner cannot access the resource in the process of variable borrowing

The following code will not compile unless we uncomment the last line of “println:”:

struct Foo {
    f: Box,
}
fn main() {
    let mut a = Foo { f: box 0 };
    // изменяемое заимствование
    let x = &mut a;
    // ошибка: не могу заимствовать `a.f` как изменяемое, т. к. `a` уже заимствовано как изменямое
    // println!("{}", a.f);
}
           { a x * }
владелец a   |_____|
заёмщик  x     |___| x = &mut a
доступ a.f       |   ошибка

This violates owner restriction # 2 (a). If we put "let x = & mut a;" into a nested block, borrowing ends before the println line! and this might work:
fn main() {
    let mut a = Foo { f: box 0 };
    {
        // изменяемое заимствование 
        let x = &mut a;
        // здесь изменяемое заимствование завершается
    }
    println!("{}", a.f);
}
           { a { x } * }
владелец a   |_________|
заёмщик  x       |_|     x = &mut a
доступ a.f           |   OK

Borrower can move variable borrowing to new borrower

This code illustrates the privileges of borrower # 2: a variable borrower x can transfer (move) a variable borrowing to a new borrower y.

fn main() {
    let mut a = Foo { f: box 0 };
    // изменяемое заимствование
    let x = &mut a;
    // переместить изменяемое заимствование в новый заёмщик y
    let y = x;
    // ошибка: использование перемещённого значения: `x.f`
    // println!("{}", x.f);
}
           { a x y * }
владелец a   |_______|
заёмщик  x     |_|     x = &mut a
заёмщик  y       |___| y = x
доступ x.f         |   ошибка

After moving, the original borrower x no longer has access to the borrowed resource.

Borrowing Area


Everything becomes interesting if we pass the links here (& and & mut), and here many beginners get confused.

Lifetime



In the entire history of borrowing, it is important to know where borrowing from the borrower begins and where it ends. In the Guide to the time of existence, this is called the time of existence: The

time of existence is a static estimate of the distance of execution, during which the pointer is valid: it always corresponds to an expression or block inside the program.

& = borrowing


A few words about borrowing. First, just remember that & = borrowing, and & mut = mutable borrowing. Wherever you see the & symbol, this is a borrowing.

Secondly, if the & symbol is shown in each structure (in its field) or in a function / closure (in its return type or captured links), then such a structure / function / closure is a borrower, and all borrowing rules apply to it.

Thirdly, for each borrowing there is an owner and a single borrower or multiple borrowers.

Extension of the loan area


A few words about the loan area. First, the loan area:
- this is the area where borrowing is effective, and
- the borrower can expand the loan area (see below).

Secondly, the borrower can expand the scope of the loan through a copy (immutable borrowing) or move (mutable loan), which occurs in assignments or in function calls. The receiver (this may be a new connection, structure, function or closure) then becomes the new borrower.

Thirdly, the loan area is the union of the areas of all borrowers, and the borrowed resource must be valid throughout the entire loan area.

Loan formula


Now, we have a loan formula:
resource area> = loan area = union of the areas of all borrowers

Code example


Let's take a look at some examples of expanding a loan area. The struct Foo structure is the same as before:

fn main() {
    let mut a = Foo { f: Box::new(0) };
    let y: &Foo;
    if false {
        // займ
        let x = &a;
        // поделиться займом с заемщиком y, следовательно расширив займы
        y = x;
    }
    // ошибка: не могу присвоить в `a.f`, потому, что он заимствован
    // a.f = Box::new(1);
}
               { a { x y } * }
    ресурс  a   |___________|
    заёмщик x       |___|     x = &a
    заёмщик y         |_____| y = x
область займа       |=======|
изменение a.f             |   ошибка

Even though the loan occurs inside the if block, and borrower x goes beyond the if block, he expanded the scope of borrowing through assignments y = x ;, so there are two borrowers: x and y. According to the loan formula, the loan area is the union of borrower x and borrower y, which is located between the first loan let x = & a; and to the end of the main unit. (Note that binding let y: & Foo; is not a borrower)

You may have noticed that the if block will never be executed, since the condition is always false, but the compiler still denies the owner of the resource `a` access to the resource. This is because all loan checks happen at compile time, you won’t do anything at runtime.

Borrowing multiple resources


So far, we have focused only on borrowing from one resource. Can a borrower take multiple resources? Of course! For example, a function can take two links and return one of them depending on certain criteria, for example, which of the links is larger than the other:

fn max(x: &Foo, y: &Foo) -> &Foo

The max function returns a & pointer, therefore it is a borrower. The return result can be any of the inbound links, so it borrows two resources.

Named Loan Area


If there are multiple & pointers as input, we must indicate their relationship using a lifetime, with the name defined in the lifetime guide . But for now, let's just call them the named loan area.

The above code will not be accepted by the compiler without indicating the relationship between borrowers, that is, those borrowers that are grouped in their loan area. This implementation will be correct:

fn max<'a>(x: &'a Foo, y: &'a Foo) -> &'a Foo {
    if x.f > y.f { x } else { y }
}
(Все ресурсы и заёмщики сгруппированы в области займа 'a'.)
                  max( {   } ) 
       ресурс *x <-------------->
       ресурс *y <-------------->
область займа 'a <==============>
       заёмщик x        |___|
       заёмщик y        |___|
возвращаемое значение     |___|   обратно к вызываемому

In this function, we have one loan area 'a' and three borrowers: two input parameters and the result returned by the function. The above borrowing formula is still applied, but now every borrowed resource must satisfy the formula. See an example below.

Code example


Let's use the max function in the following code to select the most from a and b:

fn main() {
    let a = Foo { f: Box::new(1) };
    let y: &Foo;
    if false {
        let b = Foo { f: Box::new(0) };
        let x = max(&a, &b);
        // ошибка: `b` не живет достаточно долго
        // y = x;
    }
}
               { a { b x (  ) y } }
     ресурс a   |________________| успех
     ресурс b       |__________|   провал
область займа         |==========|
временный заёмщик        |_|       &a
временный заёмщик        |_|       &b
    заёмщик x         |________|   x = max(&a, &b)
    заёмщик y                |___| y = x

Before let x = max (& a, & b); all is well, because & a and & b are temporary references that are valid only in the expression, and the third borrower x takes two resources (either a or b but with the borrower check, if both are borrowed) to the end of the if block, thus the area the loan is in let x = max (& a, & b); to the end of the if block. Resources a and b are valid throughout the loan area, therefore, satisfy the loan formula.

Now, if we uncomment the last value y = x ;, y will become the fourth borrower, and the loan area will increase to the end of the main block, as a result of which resource b will fail the loan formula test.

Structure as a borrower


In addition to functions and closures, a structure can also take up several resources while maintaining several references in its area (s). Let's look at the example below, and how the loan formula is applied. Let's use the Link structure to store the link (immutable borrow):

struct Link<'a> {
    link: &'a Foo,
}

Structure borrows several resources


Even with only one field, a Link structure can take several resources:

fn main() {
    let a = Foo { f: Box::new(0) };
    let mut x = Link { link: &a };
    if false {
        let b = Foo { f: Box::new(1) };
        // ошибка: `b` не живет достаточно долго
        // x.link = &b;
    }
}
             { a x { b * } }
    ресурс a   |___________| успех
    ресурс b         |___|   провал
область займа    |=========|
   заёмщик x     |_________| x.link = &a
   заёмщик x           |___| x.link = &b

In the above example, borrower x takes resources from owner a, and the loan area goes to the end of the main block. So far, so good. If we uncomment the last line x.link = & b ;, x will also try to borrow the resource from owner b, and then resource b will fail the loan formula test.

Function for expanding the loan area without return value


A function without a return value can also expand the scope of the loan through its input parameters. For example, the store_foo function accepts a mutable link to Link, and stores the Foo (immutable borrow) link into it:

fn store_foo<'a>(x: &mut Link<'a>, y: &'a Foo) {
    x.link = y;
}

In the following code, borrowed resources have taken possession of the resources; The Link structure mutually refers to the borrower x (i.e. * x is the borrower); The loan area goes to the end of the main block.

fn main() {
    let a = Foo { f: Box::new(0) };
    let x = &mut Link { link: &a };
    if false {
        let b = Foo { f: Box::new(1) };
        // store_foo(x, &b);
    }
}
               { a x { b * } }
      ресурс a   |___________| успех
      ресурс b         |___|   провал
область займа      |=========|
   заёмщик *x     |_________| x.link = &a
   заёмщик *x           |___| x.link = &b

If we uncomment the last line store_foo (x, & b); the function will try to store & b in x.link, making resource b another borrowed resource and fail the loan formula test, since the area of ​​resource b does not cover the entire loan area.

Multiple Loan Areas


A function may have several named loan areas. For instance:
fn superstore_foo<'a, 'b>(x: &mut Link<'a>, y: &'a Foo,
                          x2: &mut Link<'b>, y2: &'b Foo) {
    x.link = y;
    x2.link = y2;
}

This (probably not very useful) function involves two disparate loan areas. Each loan area will have its own borrowing formula.

Why lifetime is confusing


Finally, I want to explain why I think the term lifetime used by the Rust loan system is confusing (and thus avoiding the use of the term in this blog).

When we talk about a loan, there are three types of “lifetime”:

A: the lifetime of the owner of the resource (or the owner / borrowed resource)
B: the “lifetime” of the entire loan, i.e. from the first loan to the return of
C: the life of an individual borrower or borrowed index

When someone speaks of the term "lifetime", he can mean any of the above. If several resources and borrowers are involved, then everything becomes even more confusing. For example, what does a “lifetime with a name” do in a function or structure declaration? Does this mean A, B or C?

In our previous max function:

fn max<'a>(x: &'a Foo, y: &'a Foo) -> &'a Foo {
    if x.f > y.f { x } else { y }
}

What does the lifetime of 'a' mean? This should not be A, since the two resources are involved and have different times of existence. It cannot be C, because there are three borrowers: x, y and the return value of the function, and they all have different lifetimes. Does this mean B? Probably. But the entire area of ​​the loan is not a specific object, how can it have a “time of existence”? To call it the time of existence is to be mistaken.

Someone might say that this means the minimum requirements for the lifetime for borrowed resources. In some cases, this may make a difference, however, how can we call them "time of existence"?

The concept of ownership / borrowing is complex in itself. I would say that the confusion over what gives “a lifetime” makes development even more incomprehensible.

PS Using A, B, and C as defined above, the loan formula becomes:

    A >= B = C1 U C2 U … U Cn

Learning Rust is worth your while!


Although it takes a long time to learn a loan and mastery, it is interesting to study. Rust will try to achieve security without garbage collection, and it still does very well. Some people say that mastering Haskell changes the way you program. I think developing Rust is also worth your time.

Hope this post helps you.

Also popular now: