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 anErr
value, expect will cause the program to crash and display the message that you passed as an argument to expect; - if
io::Result
is anOk
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 to1..=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
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
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
#![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.