The Rust Programming Language

What

This is a digest of the Rust book. Still work-in-progress.

Why

The Rust book is a bit verbose if you already have some basic understanding of any other programming language. So, I'm trying to create a (much) shorter version of it and still cover the most critical parts. The digest also works as a "cheat sheet" in case you forget something and want to do a quick search.

That said, for readers who are inexperienced in any programming language but want to learn Rust, it's still highly recommended to read the original book because, my god, it's good.

0. Introduction

Rust usage examples:

  • command line tools
  • web services
  • DevOps tooling
  • embedded devices
  • audio and video analysis and transcoding
  • cryptocurrencies
  • bioinformatics
  • search engines
  • Internet of Things applications
  • machine learning
  • major parts of the Firefox web browser.

Rust is for people who crave speed and stability in a language. Speed: the speed of the programs that you can create with Rust and the speed at which Rust lets you write them.

1. Getting Started

1.1 Install

rustup: a command line tool for managing Rust versions and associated tools.

$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

# A C compiler is required
# because some common Rust packages depend on C code and will need a C compiler.
# On macOS, you can get a C compiler by running:
xcode-select --install

Update:

$ rustup update

Uninstall:

rustup self uninstall

Version check:

rustc --version

1.2 Write, Compile and Run

$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
$ touch main.rs

Rust files always end with the .rs extension. If you’re using more than one word in your filename, use an underscore to separate them. For example, use hello_world.rs rather thanhelloworld.rs.

Now open the main.rs file you just created and enter the code in Listing 1-1.

Filename: main.rs

fn main() {
    println!("Hello, world!");
}
  • fn
  • println! calls a Rust macro. If it called a function instead, it would be entered as println (without the !)
  • semicolon

1.3 Cargo

Check cargo's version:

cargo --version

Create a project with cargo:

$ cargo new hello_cargo
$ cd hello_cargo

Build and run:

cargo build
cargo run

Cargo also provides a command called cargo check. This command quickly checks your code to make sure it compiles but doesn’t produce an executable:

cargo check

When your project is finally ready for release, you can use cargo build --release to compile it with optimizations.

This command will create an executable in target/release instead of target/debug. The optimizations make your Rust code run faster, but turning them on lengthens the time it takes for your program to compile. This is why there are two different profiles: one for development, when you want to rebuild quickly and often, and another for building the final program you’ll give to a user that won’t be rebuilt repeatedly and that will run as fast as possible. If you're benchmarking your code's running time, be sure to run cargo build --release and benchmark with the executable in target/release.

2. Guessing Game

2.1 Bring Library into Scope

use std::io;

2.2 Mutable V.S. immutable

let apples = 5; // immutable
let mut bananas = 5; // mutable

By default variables are immutable.

2.3 Read Input

io::stdin()
    .read_line(&mut guess)
    .expect("Failed to read line");

One long line is difficult to read, so it’s best to divide it.

read_line returns a value, an io::Result. The Result types are enums. For Result, the variants are:

  • Ok: the operation was successful, and inside Ok is the successfully generated value;
  • Err: the operation failed, and Err contains information about how or why the operation failed.

expect:

  • if io::Result is an Err value, expect will cause the program to crash and display the message that you passed as an argument to expect;
  • if io::Result is an Ok value, expect will take the return value that Ok is holding and return just that value to you so you can use it.

2.4 Line Print with Placeholder


#![allow(unused)]
fn main() {
let x = 5;
let y = 10;

println!("x = {} and y = {}", x, y);
}

2.5 Adding Dependencies

In Cargo.toml file, add:

rand = "0.8.3"

Here, SEMVER is used.

Update cargo:

cargo update

2.6 RNG

use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1..101);
    println!("The secret number is: {}", secret_number);
}

2.7 gen_range

gen_range(start..end):

  • inclusive on the lower bound
  • exclusive on the upper bound
  • range 1..101 is equivalent to 1..=100

2.8 Match Expression

std::cmp::Ordering enum:

  • Less
  • Greater
  • Equal

A match expression is made up of arms:

match guess.cmp(&secret_number) {
    Ordering::Less => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal => println!("You win!"),
}

2.9 Trim & Parse: Handle Input

let guess: u32 = guess.trim().parse().expect("Please type a number!");

The trim method eliminates \n or \r\n.

  • parse method on strings: parses a string into some kind of number
  • could easily cause an error
  • returns a Result type, needs to be handled

2.10 Loop and Break

    loop {
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                break;
            }
        }
    }

3. Common Concepts

3.1. Variables and Mutability

3.1.1 Mutability

By default, variables are immutable.

Error:

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
}

Make it mutable by adding mut in front of the variable name:

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

3.1.2 Differences Between Variables and Constants

  • constants can't be mut
  • declaration: constants: const keyword
  • constants can be declared in any scope, including the global scope
  • constants may be set only to a constant expression, not the result of a function call or any other value that could only be computed at runtime

Here's an example of a constant declaration where the constant's name is THREE_HOURS_IN_SECONDS and its value is set to the result of multiplying 60 (the number of seconds in a minute) by 60 (the number of minutes in an hour) by 3 (the number of hours we want to count in this program):


#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}

3.1.3 Shadowing

You can declare a new variable with the same name as a previous variable. The first variable is shadowed by the second.

fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;
    println!("The value of x is: {}", x);
}

Shadowing V.S. mut:

  • reassign to this variable without using the let keyword will cause an error
  • we're effectively creating a new variable when we use the let keyword again, we can change the type of the value but reuse the same name. Mut can't change type.

3.2. Data Types

Rust is a statically typed language, which means that it must know the types of all variables at compile time.

The compiler can usually infer what type we want to use based on the value and how we use it. In cases when many types are possible, we must add a type annotation.

3.2.1 Scalar Types

Integer

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Integer types default to i32.

Integer literal: for example, 1000. Number literals can also use _ as a visual separator to make the number easier to read, such as 1_000, which will have the same value as if you had specified 1000.

Floating points: f32 and f64; the default type is f64 because on modern CPUs it's roughly the same speed as f32 but is capable of more precision.

Boolean :true/false

Character: four bytes in size and represents a Unicode Scalar Value, which means it can represent a lot more than just ASCII.

3.2.2 Compound Types

Tuple


#![allow(unused)]
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Tuples have a fixed length: once declared, they cannot grow or shrink in size.

Destructure:

fn main() {
    let tup = (500, 6.4, 1);
    let (x, y, z) = tup;
    println!("The value of y is: {}", y);
}

Array


#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [3; 5];
}

3.3. Functions

3.3.1 Parameters

fn main() {
    another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

3.3.2 Statements and Expressions

Function bodies are made up of a series of statements optionally ending in an expression.

Statements do not return values. Expressions evaluate to something.

Expressions do not include ending semicolons. If you add a semicolon to the end of an expression, you turn it into a statement, which will then not return a value.

3.3.3 Return Value

Declare their type after an arrow (->).

In Rust, the return value of the function is synonymous with the value of the final expression in the block of the body of a function.

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {}", x);
}

3.4. Comments

In Rust, the idiomatic comment style starts a comment with two slashes //, and the comment continues until the end of the line.

Comments can also be placed at the end of lines containing code, but you'll more often see them used with the comment on a separate line above the code it's annotating.

For comments that extend beyond a single line, you'll need to include // on each line.

3.5. Control Flow

3.5.1 if / else if

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

3.5.2 Use if in let Statement

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {}", number);
}

Both the if arm and the else arm should be the same type.

3.5.3 loop, while

fn main() {
    loop {
        println!("again!");
    }
}

Return Value from loop:

fn main() {
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    println!("The result is {}", result);
}

Conditional loop with while:

fn main() {
    let mut number = 3;
    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }
    println!("LIFTOFF!!!");
}

3.5.4 Loop through a Collection

fn main() {
    let a = [10, 20, 30, 40, 50];
    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

The use of iter().

fn main() {
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

rev to reverse the range.

4. Ownership

All programs have to manage the way they use a computer's memory while running. Some languages have garbage collection; in other languages, the programmer must explicitly allocate and free the memory. Rust uses a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks at compile time. None of the ownership features slow down your program while it's running.

A word on GC: Garbage collection frees the programmer from manually deallocating memory. This eliminates or reduces some categories of errors: dangling pointers, double free bugs, certain kinds of memory leaks, etc. The disadvantages is that, it consumes computing resources in deciding which memory to free, even though the programmer may have already known this information.

4.1 Stack & Heap

All data stored on the stack must have a known, fixed size. Data with an unknown size at compile time or a size that might change must be stored on the heap instead.

Pushing to the stack is faster than allocating on the heap. Accessing data in the heap is slower than accessing data on the stack. Allocating a large amount of space on the heap can also take time.

When your code calls a function, the values passed into the function (including, potentially, pointers to data on the heap) and the function's local variables get pushed onto the stack. When the function is over, those values get popped off the stack.

4.2 Ownership Rules

Keeping track of what parts of code are using what data on the heap, minimizing the amount of duplicate data on the heap, and cleaning up unused data on the heap so you don't run out of space are all problems that ownership addresses. Once you understand ownership, you won't need to think about the stack and the heap very often, but knowing that managing heap data is why ownership exists can help explain why it works the way it does.

  • Each value in Rust has a variable that's called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.
fn main() {
    {                      // s is not valid here, it’s not yet declared
        let s = "hello";   // s is valid from this point forward
        // do stuff with s
    }                      // this scope is now over, and s is no longer valid
}

4.3 The String Type (on the heap)

The data types mentioned in Chapter 3 are all in the stack.

The String type is on the heap.

4.4 Move and Clone

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;                // s1 "moved" to s2, doesn't do "deepcopy", but rather a shallow copy.
    println!("{}, world!", s1); // s1 can't be borrowed because it's already moved to s2
}
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

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

clone() is like deepcopy.

For stack only data, no need to clone:

fn main() {
    let x = 5;
    let y = x;  // clone; x isn't moved to y

    println!("x = {}, y = {}", x, y);  // x still available
}

If a type implements the Copy trait, an older variable is still usable after assignment. Rust won’t let us annotate a type with the Copy trait if the type, or any of its parts, has implemented the Drop trait.

Any group of simple scalar values can implement Copy, and nothing that requires allocation or is some form of resource can implement Copy.

Here are some of the types that implement Copy:

  • All the integer types, such as u32.
  • The Boolean type, bool, with values true and false.
  • All the floating point types, such as f64.
  • The character type, char.
  • Tuples, if they only contain types that also implement Copy. For example, (i32, i32) implements Copy, but (i32, String) does not.

4.5 Functions and Ownership

The semantics for passing a value to a function are similar to those for assigning a value to a variable. Passing a variable to a function will move or copy, just as assignment does.

Returning values can also transfer ownership.

4.6 References and Borrowing

& and * (dereferencing), like in other languages.

Just as variables are immutable by default, so are references. We’re not allowed to modify something we have a reference to.

Mutable reference:

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

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

Only one mutable reference to a particular piece of data in a particular scope. This code will fail:

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{}, {}", r1, r2);
}

No combining mutable and immutable references.

4.7 Dangling References

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

4.8 Rules of References

  • At any given time, you can have either one mutable reference or any number of immutable references.
  • References must always be valid.

4.9 The Slice Type

Slice also doesn't have ownership. Type: &str.

We can create slices using a range within brackets by specifying [starting_index..ending_index].

With Rust’s .. range syntax, if you want to start at index zero, you can drop the value before the two periods. In other words, these are equal:

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

    let slice = &s[0..2];
    let slice = &s[0..=1];
    let slice = &s[..2];
}

You can also drop both values to take a slice of the entire string. So these are equal:

fn main() {
    let s = String::from("hello");
    let len = s.len();
    let slice = &s[0..len];
    let slice = &s[..];
}

String literals are slices.

5. Structs

5.1 Mutable Struct:

The entire instance must be mutable; Rust doesn't allow us to mark only certain fields as mutable.

5.2 Field Init Shorthand

The main benefit of using methods instead of functions, in addition to using method syntax and not having to repeat the type of self in every method's signature, is for organization.

Use the same name as the fields:

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

fn build_user(email: String, username: String) -> User {
    User {
        email,      // field init shorthand
        username,   // field init shorthand
        active: true,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("[email protected]"),
        String::from("someusername123"),
    );
}

The String type rather than the &str string slice type is used in the example above.

This is a deliberate choice because we want instances of this struct to own all of its data and for that data to be valid for as long as the entire struct is valid.

5.3 Struct Update Syntax

fn main() {
    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("[email protected]"),
        username: String::from("anotherusername567"),
        ..user1     // struct update syntax
    };
}

5.4 Tuple Structs

Tuple structs are useful when you want to give the whole tuple a name and make the tuple be a different type from other tuples, and naming each field as in a regular struct would be verbose or redundant.

Example:

fn main() {
    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);

    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

5.5 Struct as param

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

5.6 Adding Useful Functionality with Derived Traits

In the println!, by default, the curly brackets tell println! to use formatting known as Display.

In format strings you may be able to use {:?} (or {:#?} for pretty-print instead, using the Debug trait.

We have to explicitly opt in to make that functionality available for our struct.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:#?}", rect1);
}

5.7 Method

Methods are different from functions in that they're defined within the context of a struct, and their first parameter is always self, which represents the instance of the struct the method is being called on.

The main benefit of using methods instead of functions, in addition to using method syntax and not having to repeat the type of self in every method's signature, is for organization.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        // here &self is used instead of self,
        // beacuse we only want to read data, instead of taking ownership
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

The impl block defines on which struct the method is implemented.

Each struct is allowed to have multiple impl blocks.

Methods can take ownership of self, borrow self immutably (in the code above), or borrow self mutably.

If we wanted to change the instance that we've called the method on as part of what the method does, we'd use &mut self as the first parameter.

5.8 Automatic Referencing and Dereferencing

When you call a method with object.something(), Rust automatically adds in &, &mut, or * so object matches the signature of the method.

p1.distance(&p2);
(&p1).distance(&p2);

They are the same.

5.9 Associated Functions

Functions within impl blocks that don't take self as a parameter are called associated functions because they're associated with the struct.

They're still functions, not methods, because they don't have an instance of the struct to work with.

Associated functions are often used for constructors that will return a new instance of the struct.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

6. Enums and Pattern Matching

Enums (enumerations) allow you to define a type by enumerating its possible variants.

Enums are a feature in many languages, but their capabilities differ in each language.

Rust's enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.

6.1 Enum Definition & Differences with

fn main() {
    enum IpAddrKind {
        V4,
        V6,
    }

    enum IpAddr {
        V4(u8, u8, u8, u8),
        V6(String),
    }

    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));

    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }

    impl Message {
        fn call(&self) {
            // method body would be defined here
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();
}
  • We can attach data to each variant of the enum directly.
  • Each variant can have different types and amounts of associated data.
  • We can define methods on enums.

6.2 The Option Enum V.S. Null Values

Option is another enum defined by the standard library.

Null: billion-dollar mistake. Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent. This enum is Option, and it is defined by the standard library as follows:

#![allow(unused)]
fn main() {
    enum Option<T> {
        None,
        Some(T),
    }
}

The Option<T> enum is so useful that it's even included in the prelude; you don't need to bring it into scope explicitly. In addition, so are its variants: you can use Some and None directly without the Option:: prefix. The <T> syntax: generic type parameter. <T> means the Some variant of the Option enum can hold one piece of data of any type. Here are some examples of using Option values to hold number types and string types:

fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");

    let absent_number: Option<i32> = None;
    // if we use None rather than Some,
    // we need to tell Rust what type of Option<T> we have
    // because the compiler can't infer the type,
    // that the `Some` variant will hold by looking only at a `None` value.
}

When we have a Some value, we know that a value is present and the value is held within the Some. When we have a None value, in some sense, it means the same thing as null: we don't have a valid value. We can proceed confidently without having to check for null before using that value. You have to convert an Option<T> to a T before you can perform T operations with it. Generally, this helps catch one of the most common issues with null: assuming that something isn't null when it actually is.

6.3 The match Control Flow Operator

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
        // pattern => code
        // the => operator that separates the pattern and the code (expression) to run
        // arms are executed in order
    }
}

fn main() {}

6.4 Patterns that Bind to Values

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

6.5 Matching with Option<T>

fn main() {
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

Matches in Rust are exhaustive: we must exhaust every last possibility in order for the code to be valid.

6.6 The _ Placeholder

fn main() {
    let some_u8_value = 0u8;
    match some_u8_value {
        1 => println!("one"),
        3 => println!("three"),
        5 => println!("five"),
        7 => println!("seven"),
        _ => (),
        // () is the unit value
        // so nothing will happen in the _ case
    }
}

The _ pattern will match any value. By putting it after our other arms, the _ will match all the possible cases that aren't specified before it.

6.7 if let Concise Control Flow

The if let syntax lets you combine if and let into a less verbose way to handle values that match one pattern while ignoring the rest.

fn main() {
    let some_u8_value = Some(0u8);
    match some_u8_value {
        Some(3) => println!("three"),
        _ => (),
    }
}

The following code behaves the same:

fn main() {
    let some_u8_value = Some(0u8);
    if let Some(3) = some_u8_value {
        println!("three");
    }
}

The syntax if let takes a pattern and an expression separated by an equal sign. It works the same way as a match, where the expression is given to the match and the pattern is its first arm.

Using if let means less typing, less indentation, and less boilerplate code. However, you lose the exhaustive checking that match enforces. Choosing between match and if let depends on what you’re doing in your particular situation and whether gaining conciseness is an appropriate trade-off for losing exhaustive checking.

In other words, you can think of if let as syntax sugar for a match that runs code when the value matches one pattern and then ignores all other values.

We can include an else with an if let.

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {:?}!", state),
        _ => count += 1,
    }
}

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {:?}!", state);
    } else {
        count += 1;
    }
}

They are the same.