Learning Rust

[*] 持续更新中

“Everyone can learn a language during their breakfast.” I forgot who said that. Anyway, the language itself is not so hard. I simply put down some notes about the shiny things in the tutorial. You should not treat the page as a guidance to Rust. However, you can treat this as a check page after finishing learning Rust for the first time, to check if you understand all of this better than me 🙂.

Reference

These two resources are of great help.

The Rust Programming Language

Learn X in Y minutes Where X=Rust

Rust Design and WHY

How Rust avoid double free?

How Rust avoid data race?

How Rust avoid dangling reference?

Immutable Variables

In Rust and some modern programming languages, the meaning of “variables” has changed. Traditionally, we refer the word “variable” as a notation of some undetermined, mutable value. But in Rust, we have “immutable variables”.

Immutable variables are not constants! Immutable variables are still variables and is run-time computed. They have their life-time, and can be destoried. Moreover, immutable variables can be “shadowing“ (re-bind). For example, you can use let twice to bind the literal x to a different meaning.

let x = 1;
let x = x * x;

Shadowing is more like to abandon the first value and give the name to a new value. But what’s the difference and what’s the point of this? No more details because I’m feeling like falling into an endless debate between functional programming and object-oriented programming. I could write a book on this.

Statements and Expressions

In Rust statements and expressions are divided clearly, sounds like the left values and right values in C. Statements are instructions that perform some action and do not return a value. Expressions evaluate to a resulting value. Rust is an expression-based language.

For example, let is a statement so you cannot do let y = let x = 1 sort of things. if is an expression. So you can use it like a normal expression 1 + 1

let condition = true;
let number = if condition {
    5
} else {
    6
};

Notice that a {} block is also an expression. So you can use it like

let x = 1000;

let y = {
    let x = 1;  // new scope here, does not affect the `x` outside.
    x + 1
};

loop is an expression as well. Use break to return the value.

let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2;
    }
};

Ownership

You’ve probably heard that Rust does not contain Garbage Collector like Java. Instead, Rust use ownership to protect the memory safety.

Resource Acquisition Is Initialization (RAII) is a technique developed for exception-safe resource management in C++ during 1980s. In RAII, holding a resource is a class invariant, and is tied to object lifetime. In English, that is to say, resource allocation is done during object initialization by the constructor, while resource deallocation is done during object finalization by the destructor.

Each value in Rust has a variable that’s called its owner. When the owner goes out of the scope, the value will be dropped. Rust automatically invokes a drop function after the end of every }.

Move

Move is very surprising. When you write codes like this:

let s1 = String::from("Hello");
let s2 = s1;

// println!("{}", s1);
// The code cannot compile because `s1` is moved!

You will find yourself very surprised when you can no longer use s1. The value is moved from s1 to s2 when use =.

https://doc.rust-lang.org/stable/book/img/trpl04-04.svg

This design can avoid double free, which is a common bug in C++.

Clone

Clone is very resource-consuming, as it actually clone the value in heap.

let s1 = String::from("Hello");
let s2 = s1.clone();

println!("{}, {}", s1, s2);

Copy (stack-only)

Copy works for stack-only value. It’s okay to use x after let y = x; if x is an interger, bool, float, char, or turple containing only copied value type that is stored in the stack.

Rust use Copy trait to deside which value is unusable after =. Copy trait conflicts with the Drop trait.

Ownership and function

When we pass a value to a function, it will move or copy. We we return a value from function, the return value also transfer ownership.

fn main() {
    let s1 = gives_ownership();         // gives_ownership moves its return
                                        // value into s1

    let s2 = String::from("hello");     // s2 comes into scope

    let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                        // takes_and_gives_back, which also
                                        // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
  // moved, so nothing happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                             // return value into the function
                                             // that calls it

    let some_string = String::from("hello"); // some_string comes into scope

    some_string                              // some_string is returned and
                                             // moves out to the calling
                                             // function
}

// takes_and_gives_back will take a String and return one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                      // scope

    a_string  // a_string is returned and moves out to the calling function
}

Reference

Use reference in function makes it easier for us to deal with variable ownership.

fn main() {
    let s1 = String::from("hello");
    let len = get_length(&s1);
}

fn get_length(s: &String) -> usize {
    s.len()
}

The reference doesn’t copy the value. It actually works like this:

https://kaisery.github.io/trpl-zh-cn/img/trpl04-05.svg

Reference works like “borrowing”. One cannot damage things it borrowed. So when you try to modify the borrowed value, you will fail. Make reference “mutable” is the only way that explicitly tells “Take it and you can modify it”.

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Here’s a very strict rule: you cannot create two mutable references in the same scope. This design is used to avoid data race. For the same reason, you can not have both mutable reference and immutable reference at the same scope.

Data race is caused by three behavior:

  1. Two or more pointers visit same data at the same time
  2. At least one pointer is used to write data
  3. No mechanism being used to synchronize

Rust just does not allow this happen by the very strict rule. It won’t compile if there exists data race!

Rust can also avoid dangling references by compile-time check. If you are returning a reference to a dropped value, you will fail.

Slice

Slice is a partial reference. For example, we want a reference to a string from the second letter to the forth letter, we can have &s[2..5].