I present to you the translation of the article “Rust Borrow and Lifetimes” from the blog of Arthur Liao, an engineer for Yahoo!Rust is a new programming language that has been
actively developed 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 time of existence, which confuses many newbies, including myself. This post assumes that you have a basic understanding of Rust. If not, you can first read the
Guide itself and the
Guide to Pointers .
Resource Ownership and Borrowing
In Rust, memory security is ensured without garbage collection through the use of a sophisticated borrowing system. There is at least one owner for any resource that is engaged in the release of its resources. You can create new bonds to refer to a resource using & or mut, which are called borrowing (borrow) and mutable borrow. The compiler monitors the proper behavior of all owners (owners) and borrowers (borrowers).
Copy and Move
Before we go 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, in assignments and in a function, they are called:
')
1. If the value is copied (with only primitive types, without the involvement of resources, such as processing memory or a file), the compiler copies by default.
2. Otherwise, the compiler transfers (transfers) ownership and invalidates the original binding.
In short, pod (readable old data) => copy, non-pod (linear types) => move.
Here are some additional notes for reference:
* The Rust copy method is similar to C. Each use is byte-by-copy (shady memcpy copy) instead of semantic copying or cloning.
* To make the pod structure uncopyable, you can use the NoCopy marker field or implement a Drop type.
After moving, ownership is transferred to the next owner.
Resource release
In Rust, any object is released as soon as its ownership disappears, for example, when:
1. The owner will be out of the area or
2. Ownership of the binding changes (thus, the original binding becomes void)
Privileges and restrictions of the owner (owner) and the borrower (borrower)
This section is based on the Rust Guide with reference to the copy and move methods in terms of privileges.
The owner has some privileges. And may:
1. To control the resource allocation
2. To borrow a resource unchangeably (multiple borrowings) or changeable (exclusive) and
3. Transfer ownership (with transfer).
The owner also has some limitations:
1. In the process of borrowing, the owner cannot (a) change the resource or (b) occupy it in a modified form.
2. In the process of variable borrowing, the owner cannot (a) have access to the resource or (b) occupy it.
The borrower also has some privileges. In addition to accessing or modifying a borrowed resource, the borrower may also share another borrower:
1. The borrower may allocate (copy) the immutable borrow pointer
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 the Rust code at play.rust-lang.org). In all of the following examples, we use "struct Foo", a Foo structure that is not replicable, because it contains a packed (dynamically distributed) value. The use of uncopyable resources limits the possibilities of operations, which is a good idea during the study phase.
Each code sample is also given a “region chart” to illustrate the owner’s areas, borrowers, etc. The curly brackets in the title bar are the same as the curly brackets in the code itself.
The owner can not access the resource in the process of variable borrowing
The following code will not compile unless we uncomment the last line "println:":
struct Foo { f: Box<int>, } fn main() { let mut a = Foo { f: box 0 };
This violates the restriction of owner # 2 (a). If we put "let x = & mut a;" in a nested block, borrowing ends before the println line! and it might work:
fn main() { let mut a = Foo { f: box 0 }; {
Borrower can move changeable borrowing to a new borrower.
This code illustrates borrower privileges # 2: a changeable borrower x can transfer (move) changeable borrowing to a new borrower y.
fn main() { let mut a = Foo { f: box 0 };
After the move, the original borrower x no longer has access to the borrowed resource.
Area of borrowing
Everything becomes interesting if we pass links here (& & & mut), and then many newbies get confused.
Time of existence
In the entire history of borrowing, it is important to know where borrowing of a borrower begins and where it ends. In the
Guide to Lifetime, this is called Lifetime:
The lifetime is a static estimate of the execution distance, during which the pointer is valid: it always corresponds to an expression or block within a program.
& = borrowing
A few words about borrowing. First, just remember that & = borrowing, and & mut = variable borrowing. Wherever you see the & symbol is borrowing.
Secondly, if the & symbol is shown in every 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.
Expansion of the loan area
A few words about the area of the loan. First, the loan area:
- this is the area where borrowing is effective, and
- the borrower can expand the area of the loan (see below).
Secondly, the borrower can expand the loan area through a copy (unchangeable borrowing) or relocate (a variable loan), which occurs in assignments or in function calls. The receiver (this may be a new connection, structure, function, or closure) then becomes a new borrower.
Thirdly, the loan area is a
consolidation of all borrowers from the areas, and the borrowed resource must be valid for the entire area of the loan.
Loan formula
Now, we have a loan formula:
resource area> = loan area = consolidation of all borrowers areas
Code example
Let's take a look at some examples of expanding the 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 {
Even though the loan occurs inside the if block, and the borrower x goes beyond the if block, he expanded the borrowing sphere by assigning 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; until the end of the main unit. (Note that leth: & 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 take place at compile time, you cannot do anything at run time.
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 greater 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 several pointers & as inbound, we must specify their relationship using the time of lives with the name defined in
the lifetime manual . But for now, let's just call them the named loan area.
The above code will not be accepted by the compiler without specifying the relationship between borrowers, that is, those borrowers who are grouped in their area of credit. This implementation will be correct:
fn max<'a>(x: &'a Foo, y: &'a Foo) -> &'a Foo { if xf > yf { 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 each borrowed resource must satisfy the formula. See the 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);
To let x = max (& a, & b); everything is good, because & a and & b are temporary references that are only valid in the expression, and the third borrower x borrows two resources (either a or b but checking the borrower if both are borrowed) to the end of the if block, thus The loan is in let x = max (& a, & b); to the end of the if block. Resources a and b are valid throughout the entire loan area, therefore, they 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, resulting in resource b failing the loan formula test.
Structure as a borrower
In addition to functions and closures, a structure can also occupy several resources, retaining 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 reference (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) };
In the example above, borrower x borrows resources from owner a, and the loan area goes to the end of the main unit. So far, so good. If we uncomment the last line x.link = & b ;, x also tries to borrow a resource from the owner of b, and then the resource b fails the test for a loan formula.
Function to expand the loan area without return value
A function without a return value can also expand the loan area through its input parameters. For example, the store_foo function accepts a variable link to Link, and stores the Foo (immutable borrow) link in 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 resources; The Link structure is mutable 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) };
If we uncomment the last line store_foo (x, & b); the function will try to store & b at x.link, making resource b another borrowed resource and failing the loan formula test, since the resource area b does not cover the entire loan area.
Multiple loan areas
A function may have several named loan areas. For example:
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; }
In this (probably not very useful) function, two disparate loan areas are involved. Each loan area will have its own borrowing formula.
Why time of existence is confusing
Finally, I want to explain why I think that the term “lifetime” used by Rust's loan system is confusing (and thus avoid using the term in this blog).
When we talk about a loan, there are three types of “time to live”:
A: the lifetime of the owner of the resource (or owning / borrowed resource)
B: “lifetime” of the whole loan, i.e. from first loan to return
C: the lifetime of a single borrower or borrowed index
When someone speaks of the term "lifetime", he can mean any of the above. If there are more resources and borrowers involved, then everything becomes even more confusing. For example, what does a “named lifetime” 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 xf > yf { x } else { y } }
What does 'a' lifetime mean? This should not be A, since the two resources are involved and have different times of existence. This can not be with 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 whole area of the loan is not a specific object, how can it have a “lifetime”? To call this a time of existence is to err.
Some may say that this means the minimum lifetime requirements for borrowed resources. In some cases, this may be important, but how can we call them "time of existence"?
The concept of ownership / borrowing itself is complex. I would say that the confusion in what “life time” gives makes mastering even more incomprehensible.
PS With the use of A, B, and C, as defined above, the loan formula becomes:
A >= B = C1 U C2 U … U Cn
Learning Rust is worth your time!
Although it is time consuming to study loan and acquisition, it is interesting to study. Rust will try to achieve security without garbage collection, and he still does it very well. Some people say that mastering Haskell changes your programming style. I think mastering Rust is also worth your time.
Hope this post helps you.