Notes
Search…
The Rust Programming Language
https://doc.rust-lang.org/book/title-page.html

1. Getting Started

1.2 Hello, World!

If you’re using more than one word in your filename, use an underscore to separate them.
Rust style is to indent with four spaces, not a tab.
Using a ! means that you’re calling a macro instead of a normal function.
Most lines of Rust code end with a semicolon.
Rust is an ahead-of-time compiled language, meaning you can compile a program and give the executable to someone else, and they can run it even without having Rust installed.

1.3 Hello, Cargo!

In Rust, packages of code are referred to as crates.
1
cargo new
2
cargo build [--release /* make your Rust code run faster */]
3
cargo run
4
cargo check // much faster than cargo build
5
cargo update
6
cargo doc --open
Copied!

2. Programming a Guessing Game

let statement, which is used to create a variable.
Variables, references are immutable by default.
Use mut before the variable name to make a variable mutable.
An associated function is implemented on a type, rather than on a particular instance.
The & indicates that is a reference, which gives you a way to let multiple parts of your code access one piece of data without needing to copy that data into memory multiple times.
An enumeration is a type that can have a fixed set of values, and those values are called the enum’s variants.
binary crate, which is an executable. library crate, which contains code intended to be used in other programs.
the-rust-programming-language/guessing_game at master · StoneYunZhao/the-rust-programming-language
GitHub

3. Common Programming Concepts

3.1 Variables and Mutability

By default variables are immutable.
1
let x = 5; // immutable
2
let mut y = 6; // mutable
3
4
5
// Rust’s naming convention for constants is to
6
// use all uppercase with underscores between words.
7
const MAX_POINTS: u32 = 100_000;
Copied!
Like immutable variables, constants are values that are bound to a name and are not allowed to change. But there are some difference:
  • Not allowed to use mut with constants.
  • When using the const keyword, the type of the value must be annotated.
  • 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.
You can declare a new variable with the same name as a previous variable. That means the first variable is shadowed by the second, which means that the second variable’s value is what appears when the variable is used.
1
let x = 5; // shadowed by the second x
2
let x = x + 1;
3
4
let spaces = " ";
5
let spaces = spaces.len(); // type changed
Copied!
Shadowing is different from marking a variable as mut. By using let, we can perform a few transformations on a value but have the variable be immutable after those transformations have been completed. We can change the type of the value but reuse the same name.

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.
A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.
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
Each signed variant can store numbers from -2^(n - 1) to 2^(n - 1) - 1 inclusive. Unsigned variants can store numbers from 0 to 2^(n - 1).
When you’re compiling in debug mode, Rust includes checks for integer overflow that cause your program to panic at runtime if this behavior occurs. When you’re compiling in release mode with the --release flag, Rust does not include checks for integer overflow that cause panics.
Rust’s floating-point types are 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.
A Boolean type in Rust has two possible values: true and false. Booleans are one byte in size.
Rust’s char type is the language’s most primitive alphabetic type. char literals are specified with single quotes. Rust’s char type is four bytes in size and represents a Unicode Scalar Value.
Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.
A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size.
We can use pattern matching to destructure a tuple value. We can access a tuple element directly by using a period (.) followed by the index of the value we want to access.
1
let tup: (i32, f64, u8) = (500, 6.4, 1);
2
3
let tup = (500, 6.4, 1);
4
let (x, y, z) = tup; // pattern matching
5
6
let five_hundred = tup.0;
Copied!
Unlike a tuple, every element of an array must have the same type. Arrays in Rust have a fixed length, like tuples. Arrays are useful when you want your data allocated on the stack rather than the heap.
A vector is a similar collection type provided by the standard library that is allowed to grow or shrink in size.
When you attempt to access an element using indexing, Rust will check that the index you’ve specified is less than the array length at runtime.
1
let a = [1, 2, 3, 4, 5];
2
let a: [i32; 5] = [1, 2, 3, 4, 5];
3
let a = [3; 5]; // [3, 3, 3, 3, 3]
4
5
let first = a[0];
6
let second = a[1];
Copied!

3.3 Functions

Rust code uses snake case as the conventional style for function and variable names. In snake case, all letters are lowercase and underscores separate words.
In function signatures, you must declare the type of each parameter.
Function bodies are made up of a series of statements optionally ending in an expression. Rust is an expression-based language.
Statements are instructions that perform some action and do not return a value. Expressions evaluate to a resulting value.
Function definitions are statements. Calling a function is an expression. Calling a macro is an expression. The block that we use to create new scopes, {}, is an expression. Expressions do not include ending semicolons.
1
let y = 6; // statement
2
let x = (let y = 6); // compile error
3
4
let y = {
5
let x = 3;
6
x + 1 // NOTICE: without a semicolon at the end
7
};
Copied!
The return value of the function is synonymous with the value of the final expression in the block of the body of a function. You can return early from a function by using the return keyword and specifying a value.
1
fn plus_one(x: i32) -> i32 {
2
x + 1
3
}
Copied!

3.4 Comments

1
// comments
Copied!

3.5 Control Flow

if is an expression. You must be explicit and always provide if with a Boolean as its condition. The values that have the potential to be results from each arm of the if must be the same type.
1
let number = if condition { 5 } else { 6 };
Copied!
Rust has three kinds of loops: loop, while, and for.
The loop keyword tells Rust to execute a block of code over and over again forever or until you explicitly tell it to stop.
You can add the value you want returned after the break expression you use to stop the loop; that value will be returned out of the loop so you can use it.
1
let result = loop {
2
counter += 1;
3
4
if counter == 10 {
5
break counter * 2;
6
}
7
};
8
9
while number != 0 {
10
println!("{}!", number);
11
12
number -= 1;
13
}
Copied!
You can use a for loop and execute some code for each item in a collection.
1
let a = [10, 20, 30, 40, 50];
2
3
for element in a.iter() {
4
println!("the value is: {}", element);
5
}
6
7
// range over 3, 2, 1
8
for number in (1..4).rev() {
9
println!("{}!", number);
10
}
Copied!

4. Understanding Ownership

Ownership enables Rust to make memory safety guarantees without needing a garbage collector.

4.1 What is Ownership?

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.
The stack stores values in the order it gets them and removes the values in the opposite order. All data stored on the stack must have a known, fixed size. Pushing to the stack is faster than allocating on the heap. Accessing data in the heap is slower than accessing data on the stack because you have to follow a pointer to get there.
Ownership rules:
  • 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.
A scope is the range within a program for which an item is valid.
The memory is automatically returned once the variable that owns it goes out of scope.
Rust calls drop automatically at the closing curly bracket.
1
let s1 = String::from("hello");
2
let s2 = s1; // s1 moved into s2
3
4
5
println!("s1: {}", s1) // compile error because s1 is invalid
Copied!
Rust will never automatically create “deep” copies of your data. If we do want to deeply copy the heap data of the String, not just the stack data, we can use a common method called clone.
1
let s1 = String::from("hello");
2
let s2 = s1.clone();
3
4
println!("s1 = {}, s2 = {}", s1, s2);
Copied!
Types such as integers that have a known size at compile time are stored entirely on the stack.
Rust has a special annotation called the Copy trait that we can place on types like integers that are stored on the stack. 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. 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.
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.
Returning values can also transfer ownership.
The ownership of a variable follows the same pattern every time: assigning a value to another variable moves it. When a variable that includes data on the heap goes out of scope, the value will be cleaned up by drop unless the data has been moved to be owned by another variable.

4.2 References and Borrowing

References allow you to refer to some value without taking ownership of it.
The opposite of referencing by using & is dereferencing, which is accomplished with the dereference operator, *.
When functions have references as parameters instead of the actual values, we won’t need to return the values in order to give back ownership, because we never had ownership.
We call having references as function parameters borrowing.
Just as variables are immutable by default, so are references. We’re not allowed to modify something we have a reference to.
You can have only one mutable reference to a particular piece of data in a particular scope. The benefit of having this restriction is that Rust can prevent data races at compile time.
1
// compile error
2
let mut s = String::from("hello");
3
4
let r1 = &mut s;
5
let r2 = &mut s;
6
7
println!("{}, {}", r1, r2);
Copied!
1
let mut s = String::from("hello");
2
3
{
4
let r1 = &mut s;
5
} // r1 goes out of scope here, so we can make a new reference with no problems.
6
7
let r2 = &mut s;
Copied!
We also cannot have a mutable reference while we have an immutable one.
1
let mut s = String::from("hello");
2
3
let r1 = &s; // no problem
4
let r2 = &s; // no problem
5
let r3 = &mut s; // BIG PROBLEM
6
7
println!("{}, {}, and {}", r1, r2, r3);
Copied!
A reference’s scope starts from where it is introduced and continues through the last time that reference is used.
1
let mut s = String::from("hello");
2
3
let r1 = &s; // no problem
4
let r2 = &s; // no problem
5
println!("{} and {}", r1, r2);
6
// r1 and r2 are no longer used after this point
7
8
let r3 = &mut s; // no problem
9
println!("{}", r3);
Copied!
The compiler guarantees that references will never be dangling references: if you have a reference to some data, the compiler will ensure that the data will not go out of scope before the reference to the data does.
1
// compile error: this function's return type contains a borrowed value,
2
// but there is no value for it to be borrowed from.
3
for it to be borrowed from.
4
fn main() {
5
let reference_to_nothing = dangle();
6
}
7
8
fn dangle() -> &String { // dangle returns a reference to a String
9
let s = String::from("hello"); // s is a new String
10
11
&s // we return a reference to the String, s
12
} // Here, s goes out of scope, and is dropped. Its memory goes away.
13
14
15
// solve method
16
fn no_dangle() -> String {
17
let s = String::from("hello");
18
19
s
20
} // Ownership is moved out, and nothing is deallocated.
Copied!

4.3 The Slice Type

Another data type that does not have ownership is the slice. Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection.
A string slice is a reference to part of a String. Internally, the slice data structure stores the starting position and the length of the slice. String slice range indices must occur at valid UTF-8 character boundaries.
1
let s = String::from("hello world");
2
3
let s1 = &s[0..5];
4
let s2 = &s[..6];
5
let s3 = &s[7..];
6
let s4 = &s[..];
Copied!
String literals is stored inside the binary. The type of s here is &str: it’s a slice pointing to that specific point of the binary.
1
let s = "Hello, world!"; // immutable reference.
Copied!
Defining a function to take a string slice instead of a reference to a String makes our API more general and useful without losing any functionality.
1
let a = [1, 2, 3, 4, 5];
2
3
let slice = &a[1..3]; // type: &[i32]
Copied!

5. Using Structs to Structure Related Data

A struct, or structure, is a custom data type that lets you name and package together multiple related values that make up a meaningful group.

5.1 Defining and Instantiating Structs

Rust doesn’t allow us to mark only certain fields as mutable.
Tuple structs have the added meaning the struct name provides but don’t have names associated with their fields; rather, they just have the types of the fields.
unit-like structs behave similarly to (), the unit type.
1
fn main() {
2
let mut user1 = User {
3
username: String::from("[email protected]"),
4
active: true,
5
};
6
7
user1.username = String::from("[email protected]");
8
9
let user2 = User {
10
username: String::from("anotherusername567"),
11
// struct update syntax: the remaining fields not explicitly set
12
// should have the same value as the fields in the given instance.
13
..user1
14
};
15
16
// tuple struct
17
struct Color(i32, i32, i32);
18
19
let black = Color(0, 0, 0);
20
}
21
22
fn build_user(username: string) -> User {
23
User {
24
username, // field init shorthand syntax
25
active: true,
26
}
27
}
28
29
struct User {
30
username: String, // field
31
active: bool,
32
}
Copied!

5.2 An Example Program Using Structs

1
println!("{}", user); // compile error
2
println!("{:?}", user); // should annotate with #[derive(Debug)]
3
println!("{:#?}", user); // should annotate with #[derive(Debug)]
Copied!

5.3 Method Syntax

Methods are similar to functions. But are different from functions in that they’re defined within the context of a struct (or an enum or a trait object), and their first parameter is always self, which represents the instance of the struct the method is being called on.
Having a method that takes ownership of the instance by using just self as the first parameter is rare; this technique is usually used when the method transforms self into something else and you want to prevent the caller from using the original instance after the transformation.
Associated functions are often used for constructors that will return a new instance of the struct. And let you namespace functionality that is particular to your struct without having an instance available.
Each struct is allowed to have multiple impl blocks.
1
#[derive(Debug)]
2
struct Rectangle {
3
width: u32,
4
height: u32,
5
}
6
7
impl Rectangle {
8
// If we wanted to change the instance that we’ve called the method on
9
// as part of what the method does, we’d use &mut self as the first parameter.
10
fn area(&self) -> u32 {
11
self.width * self.height
12
}
13
14
fn can_hold(&self, other: &Rectangle) -> bool {
15
self.width > other.width && self.height > other.height
16
}
17
18
// associated functions: namespaced by the struct
19
fn square(size: u32) -> Rectangle {
20
Rectangle {
21
width: size,
22
height: size,
23
}
24
}
25
}
26
27
fn main() {
28
let rect1 = Rectangle {
29
width: 30,
30
height: 50,
31
};
32
33
println!(
34
"The area of the rectangle is {} square pixels.",
35
rect1.area()
36
);
37
38
let sq = Rectangle::square(3);
39
}
Copied!

6. Enums and Pattern Matching

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

6.1 Defining an Enum

You can put any kind of data inside an enum variant: strings, numeric types, or structs.
1
enum IPAddrKind {
2
V4,
3
V6,
4
}
5
6
enum IpAddr {
7
// each variant can have different types and amounts of associated data.
8
V4(u8, u8, u8, u8),
9
V6(String),
10
}
11
12
impl IpAddr {
13
fn call(&self) {
14
// body
15
}
16
}
17
18
fn main() {
19
let four = IpAddrKind::V4;
20
21
let home = IpAddr::V4(127, 0, 0, 1);
22
23
let loopback = IpAddr::V6(String::from("::1"));
24
25
home.call();
26
}
Copied!
Rust doesn’t have the null feature that many other languages have, but it does have an enum that can encode the concept of a value being present or absent. This enum is Option<T> included in the prelude.
If we use None rather than Some, we need to tell Rust what type of Option<T> we have.
1
enum Option<T> {
2
Some(T),
3
None,
4
}
5
6
let some_string = Some("a string");
7
8
let absent_number: Option<i32> = None;
Copied!
Everywhere that a value has a type that isn’t an Option<T>, you can safely assume that the value isn’t null. This was a deliberate design decision for Rust to limit null’s pervasiveness and increase the safety of Rust code.
Option in std::option - Rust

6.2 The match Control Flow Operator

match allows you to compare a value against a series of patterns and then execute code based on which pattern matches. Patterns can be made up of literal values, variable names, wildcards, and many other things.
Match arms can bind to the parts of the values that match the pattern.
1
#[derive(Debug)] // so we can inspect the state in a minute
2
enum UsState {
3
Alabama,
4
Alaska,
5
// --snip--
6
}
7
8
enum Coin {
9
Penny,
10
Nickel,
11
Dime,
12
Quarter(UsState),
13
}
14
15
fn value_in_cents(coin: Coin) -> u8 {
16
match coin {
17
Coin::Penny => 1,
18
Coin::Nickel => 5,
19
Coin::Dime => 10,
20
Coin::Quarter(state) => { // patterns that bind to value
21
println!("State quarter from {:?}!", state);
22
25
23
}
24
}
25
}
Copied!
Matches in Rust are exhaustive: we must exhaust every last possibility in order for the code to be valid.
The _ pattern will match any value.

6.3 Concise Control Flow with if let

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.
1
let some_u8_value = Some(0u8);
2
match some_u8_value {
3
Some(3) => println!("three"),
4
_ => (),
5
}
6
7
// simplified by if let
8
9
let some_u8_value = Some(0u8);
10
if let Some(3) = some_u8_value {
11
println!("three");
12
}
Copied!
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.
1
let coin = Coin::Penny;
2
let mut count = 0;
3
match coin {
4
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
5
_ => count += 1,
6
}
7
8
9
// equivalent to if let ... else ...
10
11
let coin = Coin::Penny;
12
let mut count = 0;
13
if let Coin::Quarter(state) = coin {
14
println!("State quarter from {:?}!", state);
15
} else {
16
count += 1;
17
}
Copied!

7. Managing Growing Projects with Packages, Crates and Modules

A package can contain multiple binary crates and optionally one library crate.

7.1 Packages and Crates

A crate is a binary or library. The crate root is a source file that the Rust compiler starts from and makes up the root module of your crate.
A package is one or more crates that provide a set of functionality. A package contains a Cargo.toml file that describes how to build those crates.
A package must contain zero or one library crates, and no more. It can contain as many binary crates as you’d like, but it must contain at least one crate (either library or binary).
cargo new XX command will give us a package.
Cargo follows conventions:
  • src/main.rs: is the crate root of a binary crate with the same name as the package.
  • src/lib.rs: is the crate root of a library crate with the same name as the package.
  • src/bin directory: each file will be a separate binary crate.
If a package contains src/main.rs and src/lib.rs, it has two crates: a library and a binary, both with the same name as the package.

7.2 Defining Modules to Control Scope and Privacy

Modules can hold definitions for other items, such as structs, enums, constants, traits, functions.
1
// src/lib.rs
2
mod front_of_house {
3
mod hosting {
4
fn add_to_waitlist() {}
5
6
fn seat_at_table() {}
7
}
8
9
mod serving {
10
fn take_order() {}
11
}
12
}
13
14
15
// module tree
16
crate
17
└── front_of_house
18
├── hosting
19
│ ├── add_to_waitlist
20
│ └── seat_at_table
21
└── serving
22
├── take_order
23
├── serve_order
24
└── take_payment
Copied!
If module A is contained inside module B, we say that module A is the child of module B and that module B is the parent of module A. Notice that the entire module tree is rooted under the implicit module named crate.

7.3 Paths for Referring to an Item in the Module Tree

A path can take two forms:
  • An absolute path starts from a crate root by using a crate name or a literal crate.
  • A relative path starts from the current module and uses self, super, or an identifier in the current module.
All items (functions, methods, structs, enums, modules, and constants) are private by default. Items in a parent module can’t use the private items inside child modules, but items in child modules can use the items in their ancestor modules(include siblings).
Making the module public doesn’t make its contents public. The pub keyword on a module only lets code in its ancestor modules refer to it.
1
mod front_of_house {
2
pub mod hosting {
3
pub fn add_to_waitlist() {}
4
}
5
6
mod serving {
7
fn serve_order() {}
8
9
mod back_of_house {
10
fn fix_incorrect_order() {
11
super::serve_order();
12
}
13
}
14
15
}
16
17
pub fn eat_at_restaurant() {
18
// Absolute path
19
crate::front_of_house::hosting::add_to_waitlist();
20
21
// Relative path
22
front_of_house::hosting::add_to_waitlist();
23
}
Copied!
If we use pub before a struct definition, we make the struct public, but the struct’s fields will still be private.
If we make an enum public, all of its variants are then public.

7.4 Bringing Paths into Scope with the use Keyword

We can bring a path into a scope once and then call the items in that path as if they’re local items with the use keyword.
Paths brought into scope with use also check privacy.
You can also bring an item into scope with use and a relative path.
Bringing the function’s parent module into scope with use so we have to specify the parent module when calling the function makes it clear that the function isn’t locally defined while still minimizing repetition of the full path. On the other hand, when bringing in structs, enums, and other items with use, it’s idiomatic to specify the full path.
After the path, we can specify as and a new local name, or alias, for the type.
When we bring a name into scope with the use keyword, the name available in the new scope is private. To enable the code that calls our code to refer to that name as if it had been defined in that code’s scope, we can combine pub and use.
The standard library (std) is also a crate that’s external to our package. The name of the standard library crate is std.
We can use nested paths to bring the same items into scope in one line. We do this by specifying the common part of the path, followed by two colons, and then curly brackets around a list of the parts of the paths that differ.
If we want to bring all public items defined in a path into scope, we can specify that path followed by *, the glob operator. Glob can make it harder to tell what names are in scope and where a name used in your program was defined.
1
use crate::front_of_house::hosting; // parent module
2
3
use std::collections::HashMap; // full path
4
5
use std::io::Result as IoResult;
6
7
pub use crate::front_of_house::hosting;
8
9
use std::{cmp::Ordering, io};
10
11
use std::io::{self, Write};
12
13
use std::collections::*;
Copied!

7.5 Separating Modules into Different Files

Using a semicolon after mod xxx rather than using a block tells Rust to load the contents of the module from another file with the same name as the module.
The mod keyword declares modules, and Rust looks in a file with the same name as the module for the code that goes into that module.
the-rust-programming-language/modules at master · StoneYunZhao/the-rust-programming-language
GitHub

8. Common Collections

8.1 Storing Lists of Values with Vectors

Vectors allow you to store more than one value in a single data structure that puts all the values next to each other in memory. Vectors can only store values of the same type.
1
// initialize
2
let v: Vec<i32> = Vec::new();
3
let v = vec![1, 2, 3];
4
5
// update
6
let mut v = Vec::new();
7
v.push(5);
8
9
// get
10
let third: &i32 = &v[2];
11
let opt: Option<&i32> = v.get(2);
12
13
// iterate
14
let v = vec![100, 32, 57];
15
for i in &v {
16
println!("{}", i);
17
}
18
19
let mut v = vec![100, 32, 57];
20
for i in &mut v {
21
*i += 50;
22
}
Copied!
Adding a new element onto the end of the vector might require allocating new memory and copying the old elements to the new space.
1
let mut v = vec![1, 2, 3, 4, 5];
2
3
let first = &v[0]; // immutable borrow
4
5
v.push(6); // compile error: mutable borrow
6
7
println!("The first element is: {}", first);
Copied!
When we need to store elements of a different type in a vector, we can define and use an enum! If you don’t know the exhaustive set of types the program will get at runtime to store in a vector, the enum technique won’t work. Instead, you can use a trait object.

8.2 Storing UTF-8 Encoded Text with Strings

Strings are implemented as a collection of bytes.
Rust has only one string type in the core language, which is the string slice str that is usually seen in its borrowed form &str.
The String type, which is provided by Rust’s standard library rather than coded into the core language, is a growable, mutable, owned, UTF-8 encoded string type.
Rust’s standard library also includes a number of other string types, such as OsString, OsStr, CString, and CStr.
You can conveniently use the + operator or the format! macro to concatenate String values.
1
// initialize
2
let mut s = String::new();
3
let s2 = "initial contents".to_string();
4
let s3 = String::from("initial contents");
5
6
// update
7
s.push_str("bar");
8
s.push('l');
9
10
let s1 = String::from("Hello, ");
11
let s2 = String::from("world!");
12
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
13
14
let s1 = String::from("tic");
15
let s2 = String::from("tac");
16
let s3 = String::from("toe");
17
18
let s = format!("{}-{}-{}", s1, s2, s3); // doesn’t take ownership of any of its parameters
Copied!
The + operator uses the add method, whose signature looks something like this:
1
fn add(self, s: &str) -> String {
Copied!
The compiler can coerce the &String argument into a &str.
Rust strings don’t support indexing.
A String is a wrapper over a Vec<u8>.
You can use [] with a range to create a string slice containing particular bytes:
1
let hello = "Здравствуйте";
2
let s = &hello[0..4]; // s is &str
Copied!
If you need to perform operations on individual Unicode scalar values, the best way to do so is to use the chars method. The bytes method returns each raw byte.

8.3 Storing Keys with Associated Values in Hash Maps

The type HashMap<K, V> stores a mapping of keys of type K to values of type V.
1
use std::collections::HashMap;
2
3
let mut scores = HashMap::new();
4
scores.insert(String::from("Blue"), 10);
5
scores.insert(String::from("Yellow"), 50);
6
7
let team_name = String::from("Blue");
8
let score = scores.get(&team_name);
9
10
let teams = vec![String::from("Blue"), String::from("Yellow")];
11
let initial_scores = vec![10, 50];
12
let mut scores: HashMap<_, _> =
13
teams.into_iter().zip(initial_scores.into_iter()).collect();
14
15
for (key, value) in &scores {
16
println!("{}: {}", key, value);
17
}
Copied!
For types that implement the Copy trait, like i32, the values are copied into the hash map. For owned values like String, the values will be moved and the hash map will be the owner of those values.
The or_insert method on Entry is defined to return a mutable reference to the value for the corresponding Entry key if that key exists, and if not, inserts the parameter as the new value for this key and returns a mutable reference to the new value.
1
let mut scores = HashMap::new();
2
3
// insert or overwriting
4
scores.insert(String::from("Blue"), 10);
5
6
// insert or ignore if key exists
7
scores.entry(String::from("Blue")).or_insert(50);
Copied!

9. Error Handling

Rust groups errors into two major categories: recoverable and unrecoverable errors.
Rust has the type Result<T, E> for recoverable errors and the panic! macro that stops execution when the program encounters an unrecoverable error.

9.1 Unrecoverable Errors with panic!

When the panic! macro executes, your program will print a failure message, unwind and clean up the stack, and then quit.
You can switch from unwinding to aborting upon a panic by adding panic = 'abort' to the appropriate [profile] sections in your Cargo.toml file.
We can set the RUST_BACKTRACE environment variable to get a backtrace of exactly what happened to cause the error.

9.2 Recoverable Errors with Result

The Result enum and its variants have been brought into scope by the prelude.
1
let res = File::open("hello.txt");
2
res.unwrap_or_else(...);
3
res.unwrap();
4
res.expect("xxx");
Copied!
The ? placed after a Result value is defined to work in almost the same way as the match expressions. If the value of the Result is an Ok, the value inside the Ok will get returned from this expression, and the program will continue. If the value is an Err, the Err will be returned from the whole function as if we had used the return keyword.
Error values that have the ? operator called on them go through the from function. When the ? operator calls the from function, the error type received is converted into the error type defined in the return type of the current function. As long as each error type implements the from function to define how to convert itself to the returned error type, the ? operator takes care of the conversion automatically.
We’re only allowed to use the ? operator in a function that returns Result or Option or another type that implements std::ops::Try. The main function is special:
1
fn main() -> Result<(), Box<dyn Error>> {
2
let f = File::open("hello.txt")?;
3
4
Ok(())
5
}
Copied!

9.3 To panic! or Not to panic!

Use panic for examples, prototype code, and tests.
It would also be appropriate to call unwrap when you have some other logic that ensures the Result will have an Ok value, but the logic isn’t something the compiler understands.
It’s advisable to have your code panic when it’s possible that your code could end up in a bad state. A bad state is when some assumption, guarantee, contract, or invariant has been broken:
  • The bad state is not something that’s expected to happen occasionally.
  • Your code after this point needs to rely on not being in this bad state.
  • There’s not a good way to encode this information in the types you use.
If someone calls your code and passes in values that don’t make sense, the best choice might be to call panic! and alert the person using your library to the bug in their code so they can fix it during development. Similarly, panic! is often appropriate if you’re calling external code that is out of your control and it returns an invalid state that you have no way of fixing.
When failure is expected, it’s more appropriate to return a Result than to make a panic! call.
When your code performs operations on values, your code should verify the values are valid first and panic if the values aren’t valid.
Having lots of error checks in all of your functions would be verbose and annoying. Fortunately, you can use Rust’s type system to do many of the checks for you.

10. Generic Types, Traits, and Lifetimes

10.1 Generic Data Types

1
fn largest<T>(list: &[T]) -> T
2
3
struct Point<T> {
4
x: T,
5
y: T,
6
}
7
8
// implement for all types
9
impl<T> Point<T> {
10
fn x(&self) -> &T {
11
&self.x
12
}
13
14
// not same as in struct
15
fn mixup<U>(self, other: Point<U>) -> (T, U) {
16
(self.x, other.y)
17
}
18
}
19
20
// impelement only for f32
21
impl Point<f32> {
22
fn distance_from_origin(&self) -> f32 {
23
(self.x.powi(2) + self.y.powi(2)).sqrt()
24
}
25
}
26
27
enum Option<T> {
28
Some(T),
29
None,
30
}
31
32
enum Result<T, E> {
33
Ok(T),
34
Err(E),
35
}
Copied!
Generic type parameters in a struct definition aren’t always the same as those you use in that struct’s method signatures.
Rust implements generics in such a way that your code doesn’t run any slower using generic types than it would with concrete types.
Rust accomplishes this by performing monomorphization of the code that is using generics at compile time. Monomorphization is the process of turning generic code into specific code by filling in the concrete types that are used when compiled. The process of monomorphization makes Rust’s generics extremely efficient at runtime.
The compiler looks at all the places where generic code is called and generates code for the concrete types the generic code is called with.

10.2 Traits: Defining Shared Behavior

A trait tells the Rust compiler about functionality a particular type has and can share with other types.
1
pub trait Summary {
2
fn summarize(&self) -> String;
3
4
// with default implementation
5
fn summarize2(&self) -> String {
6
String::from("Read more...")
7
}
8
}
9
10
pub struct Tweet {
11
pub username: String,
12
pub content: String,
13
}
14
15
impl Summary for Tweet {
16
fn summarize(&self) -> String {
17
format!("{}: {}", self.username, self.content)
18
}
19
}
20
21
// traits as parameters
22
pub fn notify(item: &impl Summary) {}
23
24
// trait bound syntax, same as notify
25
pub fn notify2<T: Summary>(item: &T) {}
26
27
// multiple traits
28
pub fn notify3(item: &(impl Summary + Display)) {}
29
30
// trait bound with + syntax, same as notify3
31
pub fn notify4<T: Summary + Display>(item: &T) {}
32
33
// where syntax
34
fn f1<T, U>(t: &T, u: &U) -> ()
35
where T: Display + Clone,
36
U: Clone + Debug {}
37
38
// return traits
39
fn f2() -> impl Summary {
40
Tweet {
41
username: "a".to_string(),
42
content: "b".to_string(),
43
}
44
}
Copied!
We can implement a trait on a type only if either the trait or the type is local to our crate. But we can’t implement external traits on external types. This rule ensures that other people’s code can’t break your code and vice versa.
Default implementations can call other methods in the same trait, even if those other methods don’t have a default implementation.
You can only use impl Trait if you’re returning a single type.
By using a trait bound with an impl block that uses generic type parameters, we can implement methods conditionally for types that implement the specified traits.
1
struct Pair<T> {
2
x: T,
3
y: T,
4
}
5
6
impl<T: Display + PartialOrd> Pair<T> {
7
fn cmp_display(&self) {
8
if self.x >= self.y {
9
println!("The largest member is x = {}", self.x);
10
} else {
11