Rust’s greatest advantage over other programming languages is that it introduces a method of static analysis at compile time to determine the scope and life cycle of all objects, making it possible to destroy objects precisely when they are no longer in use, without introducing any runtime complexity.

In modern programming languages, memory allocated on the heap (interpreted as malloc) is managed in one of two ways: the user reclaims this memory by calling functions in code; Or introduce an automatic garbage collection mechanism that is managed automatically by the program at run time.

The problem with the former is that it introduces extra work to the code writer and makes it hard to avoid bugs. The problem with the latter is that it will reduce the performance of programs, especially those requiring high real-time performance.

Value types versus reference types

Most modern programming languages divide types into two types: value types and reference types.

The value type is generally similar to int/byte/bool in Java, which is a fixed size data type allocated on the stack. In Rust, each of these types implements the trait Copy to mark it as a value type.

The other type is a variable size reference type, such as a String in Java, which actually has two parts in memory: one on the heap, which contains its actual data, and the other on the stack, which is actually a memory address pointing to the actual data on the stack.

For value types, because they are stored on the function call stack, the stack is destroyed at the end of the function call, so there is no “memory management” issue. It is the reference type variables that need to be managed, because at the end of a function call, even if the address of the data stored on the stack is destroyed, the data on the heap is still there, and there will be a memory leak if the data is not processed.

RAII

RAII, short for Resource Acquisition Is Initialization, Is a common programming paradigm in C++. RAII can also be used for memory management, see the following code:

class C { public: int *value; C() { value = new int(); } ~C() { delete value; }}; void f() { auto c = C(); } int main() { c(); return 0; }Copy the code

In the code above, the C constructor allocates memory and the destructor reclaims memory, so that the memory on the heap corresponding to the class (value in this case) is tied to the lifetime of a variable. At the end of a variable’s scope, memory on the heap is also reclaimed, so we don’t need to manually reclaim memory for the value field in C in our code. In this example, as soon as the function f exits, c.value is automatically collected.

This way the code writer does not need to reclaim memory manually, and there is no additional burden when the code runs.

Rust reference types, which apply the aforementioned RAII technology, automatically empty their heap of memory when they leave the lifetime scope of a variable.

RAII does have some drawbacks, however, such as assigning C to another variable, which causes the class’s destructor to be called twice, and correctness in complex situations such as multithreading.

Move semantics

Rust’s assignment (= statement), function pass-by, and result return operations copy the contents of a value type to the target. Changes to the original value are not applied to the new value. This is the same with other common programming languages. Here’s an example:

fn main() { let a = 1; let mut b = a; b += 1; println("a: {}, b: {}", a, b); // The output is "a: 1, b: 2", and both variables can be used. }Copy the code

What about doing the above on a reference type? Let’s take String as an example:

fn main() { let a = String::from("hello"); let b = a; println! ("{}", a); }Copy the code

At this point we get a compilation error:

error[E0382]: use of moved value: `a` --> a.rs:4:20 | 3 | let b = a; | - value moved here 4 | println! ("{}", a); | ^ value used here after move | = note: move occurs because `a` has type `std::string::String`, which does not implement the `Copy` traitCopy the code

The reason for this is that, instead of making a copy of the contents of memory, these reference types “move” the data to a new variable, making the original variable unusable.

This ensures that a chunk of memory allocated on the heap has only one owner. This solves the aforementioned RAII problem of assigning a reference type variable to another type and getting memory reclaimed twice.

reference

In Rust, however, the move semantics guarantee that each reference type of data has a unique owner, but this also makes writing code difficult. Let’s say we want to write a function that evaluates the length of a String:

fn get_string_length(the_s: String) -> usize { return the_s.len(); } fn main() { let s = String::from("Hello!" ); get_string_length(s); println! ("{}'s length is {}", s, length); }Copy the code

You get an error when compiling:

error[E0382]: use of moved value: `s` --> a.rs:8:35 | 7 | let length = get_string_length(s); | - value moved here 8 | println! ("{}'s length is {}", s, length); | ^ value used here after move | = note: move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait error: aborting due to previous errorCopy the code

The reason is that when get_string_length is called, the ownership of the actual string has been transferred from the variable s to the get_string_length parameter the_s, and subsequent use of s will fail.

Of course we could modify the function so that at the end it returns not only the length of the string but also the string as an argument, so that ownership can be transferred back to the caller. Obviously, though, this would be wordy and inelegant.

For this reason Rust introduces the concept of citation. References are similar to references in C++, except that variables and types are preceded by the & prefix. We rewrite the above code with a reference:

fn get_string_length(s: &String) -> usize { return s.len(); } fn main() { let s = String::from("Hello!" ); let length = get_string_length(&s); println! ("{}'s length is {}", s, length); }Copy the code

This way the code will compile and run correctly.

In Rust, by reference, operations that previously required move semantics become BORROW semantics, and the life cycle of an object is not transferred, but only temporarily “lent” to a new place.

Variability of references

If you’ve learned Rust, you know that you can declare a variable with the prefix MUt to indicate that the variable can change.

When declaring the type of a reference, you can also prefix it with mut. It means that the reference that is lent can be modified by the borrower.

It is important to note, however, that a variable can lend only one mutable reference, and no more references (including non-mutable references) can be lent at this point. This restriction is intended to prevent data consistency problems in multi-threaded cases.