An Introduction to Rust -- Part 1

 Rust
 

An Introduction to the Rust language – part 1.


(Note: All contents in this post comes from the Rust Programming Language)

4. Understanding Ownership

What’s Ownership

Ownership Rules:

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time.
  • Where the owner goes out of scope, the value will be droped.

Memory and Allocation:

Ways Variables and Data Interact: Move

1
2
3
// This String's owner from s1 move to s2
let s1 = String::from("hi");
let s2 = s1;

Ways Variables and Data Interact: Clone

1
2
3
4
// do a deeply copy of the heap data of the String (expensive)
let s1 = String::from("hi");
let s2 = s1.clone();
println!("{}, {}", s1, s2);

Stack-Only Data: Copy

1
2
3
let x = 1;
let y = x;
println!("{}, {}", x, y);

References and Borrowing

Add ampersand before variable let the variable become a reference which refers
to the value but does not own it.

Just as variables are immutable by default, so as references.

Mutable References

1
2
3
4
5
6
7
8
fn main() {
let mut s = String::from("hi");
change(&mut s);
}

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

A reference’s scope starts from where it is introduced and continues through the
last time that reference is used.

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

Dangling References

1
2
3
4
5
6
7
8
9
10
11
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!

The Slice Type

A string slice is a reference to part of a String.

1
2
let s = String::from("hello world");
let hello = &s[0..5];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
}

fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}

String literal is slice that has &str type, so it is immutable.

Change the parameter’s type of function first_word for more general.

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string[..]);

let my_string_literal = "hello world";
let word = first_word(&my_string_literal[..]);
let word = first_word(my_string_literal);
}

fn first_word(s: &str) -> &str {
// same as above
}

Other slice:

1
2
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];

This slice has the type &[i32]


Defining and Instantiating Structs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};

user1.email = String::from("a@b.com");

Use the field init shorthand syntax when variables and fields have the same name.

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}

Create instances from other instances with struct update syntax.

1
2
3
4
5
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};

Tuple Struct:

1
2
3
4
5
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

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

Structs that don’t have any fields are called unit-like structs.

Method

Methods are similar to functions, but they are defined within the context of a
struct (or an enum or a trait obejct), and their first parameter is always
self, which represents the instance of the struct the method is being called on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}

fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

fn main() {
let rect1 = Rectangle { width: 30, height: 50};
let rect2 = Rectangle { width: 10, height: 40};
let rect3 = Rectangle { width: 60, height: 45};

println!("{}", rect1.area());
println!("{}", rect1.can_hold(&rect2));
println!("{}", rect1.can_hold(&rect3));
}

Associated Functions

Associated functions are implemented within impl blocks, which don’t take
self as a parameter. And they are associated with the struct.

1
2
3
4
5
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {width: size, heigtht: size}
}
}

Access it with :: syntax:

1
let sq = Rectangle::square(3);

:: syntax is used for both associated functions and namespaces created by modules.


6. Enums and Pattern Matching

Defining an Enum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum IpAddrKind {
V4,
V6,
}

struct IpAddr {
kind: IpAddrKind,
address: String,
}

let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};

Putting data directly into each enum variant.

1
2
3
4
5
6
7
8
enum IpAddr {
V4(String),
V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

Each variant can have different types and amounts of associated data.

1
2
3
4
5
6
7
8
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

The Option Enum:

It is defined by the standard library.

1
2
3
4
enum Option<T> {
Some(T),
None,
}

1
2
3
4
let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

The match Control Flow Operator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,
}
}
1
2
3
4
5
6
7
8
9
10
11
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

Pattern that bind to values:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[derive(Debug)] // so we can inspect the state in a minute
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
},
}
}

Call it with

1
value_in_cents(Coin::Quarter(UsState::Alaska))

Matching with Option<T>:

1
2
3
4
5
6
7
8
9
10
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);

The _ Placeholder

1
2
3
4
5
6
7
8
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
}

Concise Control Flow with if let

1
2
3
4
5
let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}
1
2
3
if let Some(3) = some_u8_value {
println!("three");
}
1
2
3
4
5
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}

same as above

1
2
3
4
5
6
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}

7. Managing Growing Projects with Packages, Crates, and Modules

Module System:

  • Packages: A Cargo feature that lets you build, test, and share crates
  • Crates: A tree of modules that produces a library or executable
  • Modules and use: Let you control the organization, scope, and privacy of
    paths
  • Paths: A way of naming an item, such as a struct, function, or module

Packages and Crates

A crate is a binary or library.

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 can contain multiple binary crates and optionally one library crate. It
must contain at least one crate (either library or binary).

Use the following command to create a package:

1
cargo new your_project_name

src/main.rs is the crate root of a binary crate with the same name as the package.
whereas, src/lib.rs the crate root of a library crate. Cargo passes the crate
root files to rustc to build the library or binary. A package can have multiple
binary crates by replacing files in the src/bin directory: each file will be a
separate binary crate.

A crate will group related functionality together in a scope so the functionality
is easy to share between multiple projects.

Defining Modules to Control Scope and Privacy

The use keyword brings a path into scope; and the pub keyword makes items public.

Modules let us organize code within a crate into groups for readability and
easy reuse. Modules also control the privacy of items, which is whether an item
can be used by outside code (public) or not (private).

To create a new library named restaurant by typing the following command:

1
cargo new --lib restaurant

In src/lib.rs:

1
2
3
4
5
6
7
8
9
10
11
12
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}

mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}

Modules can contain other modules within it. It can also hold definitions for
other items, such as structs, enums, constants, traits, or functions.

src/main.rs and src/lib.rs are called crate roots. Because the contents of
either of these two files form a module named crate at the root of the crate’s
module structure, known as the module tree.

1
2
3
4
5
6
7
8
9
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment

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.

Items (functions, methods, structs, enums, modules, and constants) in a parent
module can’t use the private items inside child modules, however, items in child
modules can use the items in their ancestor modules. All items are private by default.

Exposing Paths with the pub keyword:

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

pub fn eat_at_restaurant() {
// absolute path
crate::front_of_house::hosting::add_to_waitlist();

// relative path
front_of_house::hosting::add_to_waitlist();
}

Starting relative paths with super:

1
2
3
4
5
6
7
8
9
10
fn serve_order() {}

mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}

fn cook_order() {}
}

Making structs and enums public

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}

impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}

pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);

meal.seasonal_fruit = String::from("blueberries"); // it doesn't work
}

In contrast, if we make an enum public, all of its variants are then public.

1
2
3
4
5
6
7
8
9
10
11
mod back_of_house {
pub enum Apptizer {
Soup,
Salad,
}
}

pub fn eat_at_restaurant() {
let order1 = back_of_house::Apptizer::Soup;
let order2 = back_of_house::Apptizer::Salad;
}

Bringing paths into scope with the use keyword:

1
2
3
4
5
6
7
8
9
10
11
12
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

Providing new names with the as keyword:

1
2
3
4
5
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {}
fn function2() -> IoResult<()> {}

Re-exporting names with pub use:

When we bring a name into scope with the use keyword, the name available in the
new scope is private. To make it available for other code that calls our code,
we can combine pub and use.

1
2
3
4
5
6
7
8
9
10
11
12
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

Using external packages:

Listing external packages’ name in your package’s Cargo.toml file and using
use to bring items into scope.

The standard library (std) is also a crate that’s external to our package, but
it is shipped with the Rust language. So we only need to bring items within it
into scope with use.

1
use std::collections::HashMap;

Using nested paths to clean up large use lists:

1
use std::{cmp::Ordering, io};

1
use std::io::{self, Write};

equal to

1
2
use std::io;
use std::io::Write;

The glob operator:

Bring all public items defined in a path into scope.

1
use std::collections::*;

Separating Modules into Different Files

In src/lib.rs:

1
2
3
4
5
6
7
8
9
10
// declaring the "front_of_house" module whose body will be in
// "src/front_of_house.rs" file
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist() {};
hosting::add_to_waitlist() {};
}

In src/front_of_house.rs:

1
2
3
pub mod hosting {
pub fn add_to_waitlist() {}
}

You can continue this process:

In src/front_of_house.rs:

1
pub mod hosting;

In src/front_of_house/hosting.rs:

1
pub fn add_to_waitlist() {}

Rust lets you split a package into multiple crates and a crate into modules.


8. Common Collections

Unlike the build-in array and tuple types, the data these collections point to
is stored on the heap, which means the amount of data does not need to be known
at compile time and can grow or shrink as the program runs.

  • A vector allows you to store a variable number of values next to each other.
  • A string is a collection of characters.
  • A hash map allows you to associate a value with a particular key.

Storing Lists of Values with Vectors

Vec<T>:

1
let v: Vec<i32> = Vec::new() // must add type annotation here

using vec! macro

1
2
// the type of v is Vec<i32>
let v = vec![1, 2, 3];

1
2
3
let mut v = Vec::new();
v.push(2);
v.push(3);

A vector is freed when it goes out of scope. When it gets dropped, all of its
contents are also dropped.

1
2
3
{
let v = vec![1, 2, 3];
} // v goes out of scope and is freed here

There are two ways to reference a value stored in a vector: indexing syntax and
get method (return an Option<&T>).

1
2
3
4
5
6
7
8
9
10
11
let v = vec![1, 2, 3, 4, 5];

// indexing syntax
let third: &i32 = &v[2];
println!("The third element is {}", third);

// "get" method
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}

1
2
3
4
let v = vec![1, 2, 3, 4];

let does_not_exist = &v[10]; // crash
let does_not_exist = v.get(10); // return "None"

The following code can’t work:

1
2
3
4
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; // immutable borrow
v.push(6); // mutable borrow
println!("The first element is: {}", first);

Iterating over the values in a vector:

1
2
3
4
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}

1
2
3
4
let mut v = vec![1, 2, 3];
for i in &mut v {
*i += 50; // "*" is dereference operator
}

Using an enum to store multiple types:

1
2
3
4
5
6
7
8
9
10
11
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}

let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];


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 thepanic!
macro that stops execution when the program encounters an unrecoverable error.

Unrecoverable Errors with panic!

Using a panic! Backtrace

1
RUST_BACKTRACE=1 cargo run

Recoverable Errors with Result

1
2
3
4
5
6
7
8
9
10
use std::fs::File;

fn main() {
let f = File::open("hello.txt");

let f = match f {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}

Matching on Different Errors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::fs::File;
use std::io::ErrorKind;

fn main() {
let f = File::open("hello.txt");

let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}

same as above (prefered):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::fs::File;
use std::io::ErrorKind;

fn main() {
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}

Shortcuts for Panic on Error: unwrap and expect

If the Result value is the Ok variant, unwrap will return the value inside
the Ok. If the Result is the Err variant, unwrap will call the panic!
macro.

1
2
3
4
5
use std::fs::File;

fn main() {
let f = File::open("hello.txt").unwrap();
}

expect, which is similar to unwrap, let us also choose the panic! error message.

1
2
3
4
5
use std::fs::File;

fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

Propagating Errors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::fs::File;
use std::io;
use std::io::Read;

fn main() {}

fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};

let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}

A Shortcut for Propagating Errors: the ? Operator

Placing the ? after a Result. If the value of the Result is an Ok, the
value inside the Ok will get returned from this expression. If the value is an
Err, the Err will be returned from the function, in other words, gets propagated.

same as above:

1
2
3
4
5
6
7
8
9
10
11
12
use std::fs::File;
use std::io;
use std::io::Read;

fn main() {}

fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}

shorter:

1
2
3
4
5
6
7
8
9
10
11
use std::fs::File;
use std::io;
use std::io::Read;

fn main() {}

fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}

another way:

1
2
3
4
5
6
7
8
use std::fs;
use std::io;

fn main() {}

fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}

The ? Operator Can Be Used in Functions That Return Result

One valid return type for main is (), and conveniently, another valid return
type is Result<T, E>.

1
2
3
4
5
6
7
use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}

To panic! or Not to panic!

Create Custom Types for Validation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub struct Guess {
value: i32,
}

impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}

Guess {
value
}
}

pub fn value(&self) -> i32 {
self.value
}
}

10. Generic Types, Traits, and Lifetimes

Generic Data Types

In function definitions:

1
2
3
4
5
6
7
8
9
10
fn largest<T: PartialOrd>(list: &[T]) -> T {
let mut largest = list[0];

for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}

In struct definitions:

1
2
3
4
struct Point<T, U> {
x: T,
y: U,
}

In enum definitions:

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

In method definitions:

1
2
3
4
5
6
7
8
9
10
struct Point<T> {
x: T,
y: T,
}

impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}

or we could implement methods only on Point instances

1
2
3
4
5
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

Traits: Defining Shared Behavior

Defining a trait:

1
2
3
pub trait Summary {
fn summarize(&self) -> String;
}

Implementing a trait on a type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}

impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}

//
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}

impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}

Warning:
One restriction to note with trait implementations is that we can implement a
trait on a type only if either the trait or the type is local to our crate. In
other words, we can’t implement external traits on external types.

Default implementations:

1
2
3
4
5
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}

To use a default implementation

1
impl Summary for NewsArticle {}

Default implementations can call other methods in the same trait, even if those
other methods don’t have a default implementation.

1
2
3
4
5
6
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}

1
2
3
4
5
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.usename)
}
}

Traits as parameters:

Define a notify funciton that calls the summarize method on its item
parameter, which is of some type that implements the Summary trait. Use
impl Trait syntax.

1
2
3
pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}

(item parameter accepts any type that implements the specified trait.)

Trait bound syntax:

1
2
3
pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}

Specifying multiple trait bounds with the + syntax:

1
pub fn notify(item: impl Summary + Display) {}

1
pub fn notify<T: Summary + Display>(item: T) {}

Clearer trait bounds with where clauses:

1
2
3
4
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}

instead of

1
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {}

Returning types that implement traits:

1
2
3
4
5
6
7
8
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}

Using trait bounds to conditionally implement methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::fmt::Display;

struct Pair<T> {
x: T,
y: T,
}

impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self {
x,
y,
}
}
}

impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}

blanket implementations:
(conditionally implement a trait for any type that implements another trait.)

1
2
3
impl<T: Display> ToString for T {
// --snip--
}

Validating References with Lifetimes

Preventing Dangling References with Lifetimes

The following code is wrong

1
2
3
4
5
6
7
8
9
10
{
let r;

{
let x = 5;
r = &x;
}

println!("r: {}", r);
}

The Borrow Checker

The Rust compiler has a borrow checker that compares scopes to determine
whether all borrows are valid.

Generic Lifetimes in Functions

Lifetime Annotation Syntax

1
&'a mut i32 // a mutable reference with an explicit lifetime

Lifetime Annotations in Function Signatures

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

When we pass concrete references to longest, the concrete lifetime that is
substituted for ‘a is the part of the scope of x that overlaps with the scope
of y.

Thinking in Terms of Lifetimes

When returning a reference from a function, the lifetime parameter for the return
type needs to match the lifetime parameter for one of the parameters.

Lifetime Annotations in Struct Definitions

It’s possible for structs to hold references, but in that case we would need to
add a lifetime annotation on every reference in the struct’s definition.

1
2
3
4
5
6
7
8
9
10
11
struct ImportantExcerpt<'a> {
part: &'a str,
}

fn main() {
let novel = String::from("Call me Rick. Some years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find a '.'");
let i = ImportantExcerpt { part: first_sentence };
}

(This annotation means an instance of ImportantExcerpt can’t outlive the
reference it holds in its part field.)

Lifetime Elision

Lifetimes on function or method parameters are called input lifetimes, and
lifetimes on return values are called output lifetimes.

The compiler uses three rules to figure out what lifetimes references have when
there aren’t explicit annotations. The first one is that each parameter that is
a reference gets its own lifetime parameter. The second one is if there is exactly
one input lifetime parameter, that lifetime is assigned to all output lifetime
parameters. The third one is if there are multiple input lifetime parameters, but
one of them is &self or &mut self because this is a method, the lifetime of
self is assigned to all output lifetime parameters.

Lifetime Annotations in Method Definitions

1
2
3
4
5
6
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}

(The return type gets the lifetime of &self)

The Static Lifetime

'static lifetime means that this reference can live for the entire duration of
the program. All string literals have the 'static lifetime.

1
let s: &'static str = "I have a static lifetime.";

Generic Type Parameters, Trait Bounds, and Lifetimes Together

1
2
3
4
5
6
7
8
9
10
11
12
use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: Display
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}

11. Writing Automated Tests

How to Write Tests

There are some features can be used for writing tests, include the test attribute,
a few macros, and the should_panic attribute.

The Anatomy of a Test Function

At its simplest, a test in Rust is a function that’s annotated with the test
attribute. Add #[test] on the line before fn to change a function into a test
function. When you run your tests with the cargo test command, Rust builds a
test runner binary that runs the functions annotated with the test attribute
and reports on whether each test function passes or fails.

When we make a new library project with Cargo, a test module with a test function in it is automatically generated for us.

1
cargo new adder --lib

auto generated file src/lib.rs:

1
2
3
4
5
6
7
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

Checking Results with the assert! Macro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle { width: 8, height: 7 };
let smaller = Rectangle { width: 5, height: 1 };

assert!(larger.can_hold(&smaller));
}
}

Testing Equality with the assert_eq! and assert_ne! Macros

1
2
3
4
5
6
7
8
9
10
11
12
13
pub fn add_two(a: i32) -> i32 {
a + 2
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
}

All the primitive types and most of the standard library types implement PartialEq
and Debug traits. Both two traits are derivable, so we can add
#[derive(PartialEq, Debug)] annotation to your struct or enum definition for
comparing in asset_eq! or assert_ne!.

Adding Custom Failure Messages

We can also add a custom message to be printed with the failure message as
optional arguments to the assert!, assert_eq!, and assert_ne! macros.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pub fn greeting(name: &str) -> String {
// format!("Hello {}!", name)
String::from("Hello")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`", result
);
}
}

Checking for Panics with should_panic

Adding another attribute, should_panic, to our test function. This attribute
makes a test pass if the code inside the function panics.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pub struct Guess {
value: i32,
}

impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}

Guess {
value
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}

To make should_panic tests more precise, we can add an optional expected
parameter to the should_panic attribute.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pub struct Guess {
value: i32,
}

impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1, got {}.",
value
);
} else if value > 100 {
panic!(
"Guess value must be less than or equal to 100, got {}.",
value
);
}

Guess { value }
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}

Using Result<T, E> in Tests

1
2
3
4
5
6
7
8
9
10
11
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}

Controlling How Tests Are Run

Some command line options go to cargo test, and some go to the resulting test
binary. To separate these two types of arguments, you list the arguments that go
to cargo test followed by the separator -- and then the ones that go to the
test binary.

1
2
cargo test --help
cargo test -- --help

Running Tests in Parallel or Consecutively

When you run multiple tests, by default they run in parallel using threads.
Specify the number of threads you want to use to the test binary:

1
cargo test -- --test-threads=1

Showing Function Output

If we want to see printed values for passing tests as well, we can disable the
output capture behavior by using the nocapture flag:

1
cargo test -- --nocapture

Running a Subset of Tests by Name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pub fn add_two(a: i32) -> i32 {
a + 2
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn add_two_and_two() {
assert_eq!(4, add_two(2));
}

#[test]
fn add_three_and_two() {
assert_eq!(5, add_two(3));
}

#[test]
fn one_hundred() {
assert_eq!(102, add_two(100));
}
}

running single tests:

1
cargo test one_hundred

filtering to run multiple tests:

1
cargo test add

(This command ran all tests with add in the name.)

We can run all the tests in a module by filtering on the module’s name.

Ignoring Some Tests Unless Specifically Requested

Using the ignore attribute to exclude tests:

1
2
3
4
5
6
7
8
9
10
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}

If we want to run only the ignored tests, we can use

1
cargo test -- --ignored

Test Organization

Two main test categories: unit tests and integration tests in Rust.

Unit tests are small and more focused, testing one module in isolation at a
time, and can test private interfaces. Integration tests are entirely external
to your library and use your code in the same way any other external code would,
using only the public interface and potentially exercising multiple modules per test.

Unit Tests

You’ll put unit tests in the src directory in each file with the code that
they’re testing. The convention is to create a module named tests in each file
to contain the test functions and to annotate the module with cfg(test).

The Tests Module and #[cfg(test)]

The #[cfg(test)] annotation on the tests module tells Rust to compile and run
the test code only when you run cargo test, not when you run cargo build.
Because integration tests go in a different directory, they don’t need the
#[cfg(test)] annotation.

Testing Private Functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}

Integration Tests

In Rust, integration tests are entirely external to your library.They use your
library in the same way any other code would, which means they can only call
functions that are part of your library’s public API.

To create integration tests, you first need a tests directory.

The tests Directory

Cargo will compile each of the files in the tests directory as an individual crate.

tests/integration_test.rs:

1
2
3
4
5
6
use adder; // bring our library (i.e. adder) into this test crate's scope

#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}

We don’t need to annotate any code in the files within the tests directory with
#[cfg(test)].

We can still run a particular integration test function by specifying the test
function’s name as an argument to cargo test. To run all the tests in a
particular integration test file, use the --test argument of cargo test
followed by the name of the file.

1
cargo test --test integration_test
Submodules in Integration Tests

Files in subdirectories of the tests directory don’t get compiled as separate
crates or have sections in the test output.

tests/common/mod.rs:

1
2
3
pub fn setup() {
// snip
}

tests/integration_test.rs:

1
2
3
4
5
6
7
8
9
use adder;

mod common;

#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}

Integration Tests for Binary Crates

If our project is a binary crate that only contains a src/main.rs file and
doesn’t have a src/lib.rs file, we can’t create integration tests in the
tests directory and bring functions defined in the src/main.rs file into
scope with a use statement. Only library crates expose functions that other
crates can use; binary crates are meant to be run on their own.

This is one of the reasons Rust projects that provide a binary have a
straightforward src/main.rs file that calls logic that lives in the
src/lib.rs file. Using that structure, integration tests can test the library
crate with use to make the important functionality available.


References

The Rust Programming Language