There are three principles of Rust Ownership to keep in mind.

  • For each value, there is an owner.
  • There can only be one owner for a value at a time.
  • When the owner leaves the scope, the corresponding value is automatically dropped.

rust

hello is a string allocated on the heap, and the owner is s, which, when passed as an argument to the function takes_ownership, moves ownership to some_string . That is, the hello string will be dropped when the function returns, so main will report an error if it prints line 4 because it has been moved.

What if main wants to continue printing hello? takes_ownership can return hello out, so the owner will move again to s . But it’s really inconvenient.

Introduction to references

This is where references come in, not moving ownership, just lending data.

1
2
3
4
5
6
7
8
9
fn main() {
    let s = String::from("hello");
    no_takes_ownership(&s);
    println!("main {}", s)
}

fn no_takes_ownership(some_string: &str) {
    println!("func {}", some_string);
}
1
2
3
4
5
6
zerun.dong$ cargo run
   Compiling hello_cargo v0.1.0 (/Users/zerun.dong/code/rusttest/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 1.86s
     Running `target/debug/hello_cargo`
func hello
main hello

The final runs all print, but no_takes_ownership does not own the owner, and &str represents a read-only reference to the string. no_takes_ownership(&s) , where instead of writing s directly, it takes a reference to &s.

Pointer and reference

1
2
3
4
fn main() {
    let a = 0;
    let _r = &a;
}

The above is the simplest example of a reference, where a is an int32 type with value 0. Then _r is a reference to a.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Dump of assembler code for function hello_cargo::main:
src/main.rs:
1	fn main() {
   0x000055555555b6c0 <+0>:	sub    $0x10,%rsp

2	    let a = 0;
   0x000055555555b6c4 <+4>:	movl   $0x0,0x4(%rsp)

3	    let _r = & a;
=> 0x000055555555b6cc <+12>:	lea    0x4(%rsp),%rax
   0x000055555555b6d1 <+17>:	mov    %rax,0x8(%rsp)

4	}
   0x000055555555b6d6 <+22>:	add    $0x10,%rsp
   0x000055555555b6da <+26>:	retq
End of assembler dump.

You can see that the a variable is assigned on the stack rsp + 0x4, with an initial value of 0, and then line 3 is disassembled to show that lea takes the address of a and passes it to _r on the stack.

Essentially, a rust reference is not much different from a normal pointer, but at the compilation stage, various rules are added, such as the reference cannot be null.

Borrowing rules

reference (reference) does not acquire ownership, insisting on a single owner and single responsibility, solving the shared access barrier. Passing objects by reference is called borrow, which is more efficient than transferring ownership.

  • The lifetime of a reference must not exceed the time it is referenced. This is obvious, to prevent dangling references.
  • If there is a mutable borrow of a value, then no other references (read or write) are allowed within the scope of that borrow.
  • In the absence of mutable borrowing, multiple immutable borrowings of the same value are allowed.
1
2
3
4
5
6
7
8
9
fn main() {
    let y: &i32;
    {
        let x = 5;
        y = &x;
    }

    println!("{}", y);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
zerun.dong$ cargo run
   Compiling hello_cargo v0.1.0 (/Users/zerun.dong/code/rusttest/hello_cargo)
error[E0597]: `x` does not live long enough
 --> src/main.rs:5:13
  |
5 |         y = &x;
  |             ^^ borrowed value does not live long enough
6 |     }
  |     - `x` dropped here while still borrowed
7 |
8 |     println!("{}", y);
  |                    - borrow later used here

error: aborting due to previous error

This example will report an error for the obvious reason that y has to refer to the variable x, but x is in the statement block {} and becomes invalid when it leaves this lexical scope, so y becomes a dangling null reference.

1
2
3
4
5
6
fn main(){
    let mut a = String::from("hello");
    let a_ref = &mut a;
    println!("a is {}", a);
    a_ref.push('!');
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
zerun.dong$ cargo run
   Compiling hello_cargo v0.1.0 (/Users/zerun.dong/code/rusttest/hello_cargo)
error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
 --> src/main.rs:4:25
  |
3 |     let a_ref = &mut a;
  |                 ------ mutable borrow occurs here
4 |     println!("a is {}", a);
  |                         ^ immutable borrow occurs here
5 |     a_ref.push('!');
  |     ----- mutable borrow later used here

In the above code, a_ref is a mutable borrow, and then a_ref.push is called to modify the string, while the original owner a is printed in number 4, and then an error is reported.

The reason for this is that a_ref is a mutable borrow and no other immutable or mutable borrow is allowed in its scope, where println! is an immutable borrow of a.

What confused me at first was how big the scope was! The ancient version was lexical scope, but it was improved to start with the first borrow and end with the last call, so the scope is much smaller.

1
2
3
4
5
6
fn main(){
    let mut a = String::from("hello");
    let a_ref = &mut a;
    a_ref.push('!');
    println!("a is {}", a);
}
1
2
3
4
5
zerun.dong$ cargo run
   Compiling hello_cargo v0.1.0 (/Users/zerun.dong/code/rusttest/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 1.51s
     Running `target/debug/hello_cargo`
a is hello!

After modification, just put println after push.

Confusing points

A structure can also have references in it, and it’s easy to get confused at first.

1
2
3
4
struct Stu{
  Age: int32
  Name: &str
}

In this case Name is a string reference, so the instantiated Stu object does not have ownership of Name, so it has to conform to the borrowing rules above. To be clear, it is a question of who is responsible for releasing the content.

There is also a type of method, the first parameter should be written as &self or &mut self , if written as self then the function will capture the ownership of the type, the function is executed once, it can no longer use the type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Number{
    num: u8
}

impl Number {
    fn get_num(self) -> u8 {
        self.num
    }
}

fn main(){
    let num = Number{num:10};
    println!("get num {}", num.get_num());
    println!("get num {}", num.get_num());
}

The above code is simple, the Number type has a get_num method to get the value of the variable num, but main will report an error when compiled if println is called twice.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
zerun.dong$ cargo run
   Compiling hello_cargo v0.1.0 (/Users/zerun.dong/code/rusttest/hello_cargo)
error[E0382]: use of moved value: `num`
  --> src/main.rs:15:28
   |
13 |     let num = Number{num:10};
   |         --- move occurs because `num` has type `Number`, which does not implement the `Copy` trait
14 |     println!("get num {}", num.get_num());
   |                                --------- `num` moved due to this method call
15 |     println!("get num {}", num.get_num());
   |                            ^^^ value used here after move
   |
note: this function consumes the receiver `self` by taking ownership of it, which moves `num`
  --> src/main.rs:7:16
   |
7  |     fn get_num(self) -> u8 {
   |                ^^^^

You can see that the compiler gives hints, because get_num(self) defined as self will take ownership, which moves num.

So the first argument of the type method has to be a reference, and Go writers are sure to be confused. Similarly there is the match statement block, which also moves the image and requires a reference.