The Rust Book

Getting started with Rust

You can read the Rust book in full here.

Getting Started

You can run rustup doc to access the documentation, even when offline.

Make a hello world via the following:

// main.rs
fn main() {
println!("Hello, world!");
}

Then run:

rustc main.rs
./main

You can run rustfmt from the command line to format files. This is a VS Code extension for Rust projects, whereas this one works for single files.

Macros are invoked like println!("Hello world!") whereas functions are invoked without the exclamation mark. More on those later.

Rust is a compiled language, which means you compile a binary that anyone can then run (even without Rust installed). Running is thus two steps: compiling and running, vs how it is one step for dynamic languages like JavaScript, Ruby.

Cargo is Rust's build system and package manager. Its config file is written in TOML, which stands for Tom's Obvious, Minimal Language. Great name.

Best practice is to initialize a new Rust project via cargo new my_project.

To build a cargo project, run the following:

cargo build
./target/debug/my_project

Or you can just use:

cargo run

If you want to just see if it compiles correctly, run:

cargo check

To make an optimized release build, run:

cargo build --release

Programming a guessing game

To import a package (in this case the io package from the standard library), use:

use std::io;

Certain parts of the standard library are always imported, and known as the prelude.

Rust variables are immutable by default (nice). To make one:

let foo = 5;

Make it mutable:

let mut foo = 5;

In the below code:

let mut string = String::new();

... we're calling the new associated function on the String type. An associated function belongs to a type, not an instance. AKA a static method.

Many functions can return a Result type. Result types are enums, with either the value of Ok or Err.

If you want an Err Result to crash your program with a useful message, you can use expect. Here's an example of reading the user's CLI and showing a message on a failure:

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

(&mut guess is declaring a mutable reference. More on those later.)

Unhandled Results will result in a warning at compile time. (expect isn't really "handling" the error, per se, but it will suppress the warning; if you write expect, you want the program to crash when the Result is wrong.)

You can do string interpolation like so:

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

Rust calls packages "crates". Your project is a "binary crate"; it's meant to be executed. Packages you use in your project are "library crates"; they're meant to be used by other programs.

To add a dependency, modify Cargo.toml like so:

[dependencies]
rand = "0.5.5"

You can view crates via Crates.io.

Your crate versions will stay the same thanks to Cargo.lock. To upgrade all your packages, run cargo update. Note that this will only upgrade minor version. 0.5.5 will become 0.5.6, but never 0.6.0; you have to manually upgrade to 0.6.0 via Cargo.toml.

Here's another example of importing and using a crate:

use rand::Rng;
let secret_number = rand::thread_rng().gen_range(1, 101);

We import the Rng trait of the rand crate; that must be in scope to allow us to use methods related to random number generators. We then call the rng::thread_rng function that gives us a random number generator that is local to the current thread. Then we call its gen_range method.

To see documentation for YOUR dependencies, run cargo doc --open. Pretty neat.

The cmp method can be used to compare two things, for example:

"abc".cmp("cde")

The result of a cmp will be variant of the Ordering enum (imported with use std::cmp::Ordering). The variants are Less, Equal, and Greater.

You can combine the Ordering enum with a match statement. A match statement decides what to do based on which variant is returned. Here's an example:

match "abc".cmp("abc") {
Ordering::Less => println!("Smaller!"),
Ordering::Greater => println!("Bigger!"),
Ordering::Equal => println!("Equal!"),
}

match checks each "arm" and sees if the returned variant matches; if yes, it executes that code. Here, it would print "Equal!" In the next code snippet, it would print "Smaller!"

match 1.cmp(&2) {
Ordering::Less => println!("Smaller!"),
Ordering::Greater => println!("Bigger!"),
Ordering::Equal => println!("Equal!"),
}

(The & is due to 2 needing to be a reference; more on that later.)

Rust is strongly typed, with type inference. In the below code, Rust is smart enough to guess the variable is a string:

let mut guess = String::new();

Here's an example of converting one type to another. Say that guess ends up being a number in string form, like '23'. We can convert it to a 32-bit number type like so:

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

We trim the whitespace, parse the number, and throw a descriptive error if that fails. We also clearly declare the type of the variable. If we didn't specify u32, Rust wouldn't know what kind of number to return from parse().

To create an infinite loop, enclose code in a loop {}. You can then use break to escape it.

To properly handle errors, you can move from an expect call to a match expression. Here's an example:

let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};

If Ok, we tell parse() to just return the number. If there's an Err, no matter what it is, we just continue on. Now the program will never break... well, we'll see.

Common programming concepts in Rust

Variables and mutability

Good explanation of the tradeoff of immutability: with large data structures, constantly making new immutable versions may be expensive performance-wise; however, the code is easier to reason about. With small data structures it's a no brainer: the performance cost is worth it.

Variables in Rust are immutable by default, but Rust also supports constants. Constants cannot be made mutable. They are declared via const, and the type must be specified.

const MAX_POINTS: u32 = 100_000;

Constants can be declared in any scope, including global scope: good for globally used values. They cannot be assigned as the result of a function call, or any compute value. In other words, you need to know what the constant is going to be from the moment you write it.

Constants also endure for the length of your program. They use the uppercase style seen above. So they're a great fit for unchanging values that multiple parts of your app needs to know about.

Rust also supports something called shadowing. Here's an example:

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

We overwrite x, an immutable variable, by repeating the let keyword. This means we can perform a few transformations, but still end up with an immutable variable.

This essentially creates a new variable, but with the same name.

By shadowing, you can change a variable's type, which is not allowed for mutable variables (they must always stay the same type). Here's an example:

let spaces = " ";
let spaces = spaces.len();

The Rust book claims this is superior to having to name spaces_str and spaces_num. Is it? I'm not convinced, yet. Seems somewhat dangerous. What if you forget you have an existing variable and accidentally shadow it?

Data types

Scalar types: integers, floats, booleans, and characters.

Compound types: tuples and arrays.

With integers, they can be 8, 16, 32, 64, 128, or arch in size. Arch depends on the architecture you're using: 64-bit or 32-bit.

An integers can be signed or unsigned. Unsigned means it cannot be negative. Signed means we also need to store info about whether it's positive or negative.

So you end up with the following integer types: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize.

The default type is i32, a good choice most of the time. You might use isize or usize when indexing a collection, to get the best performance from the architecture.

For floats, it's just f32 or f64, defaulting to f64.

For booleans, it's real simple:

let t = true;
let f: bool = false;

You don't need the type declaration, but can add it.

The char type is used for a single character, declared with single quotes (vs string literals, which are double quotes):

fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
}

Tuples are a fixed-length grouping of variables. Each position has a specific type.

fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

You can use type inference, as well. Here's how you access values:

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

Another way to access:

fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}

Arrays are also fixed-length, and must all be the same type.

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

Arrays are allocated to the stack instead of the heap. Most of the time, you'll want to use a vector (a type provided by the standard library) instead of an array, since a vector can grow or shrink in size.

Arrays are good for when the length isn't going to change.

Here's how you'd declare the type:

let a: [i32; 5] = [1, 2, 3, 4, 5];

This means this array has five elements of type i32.

You can also create an array of a certain length with all the same values:

let a = [3; 5];

This will create an array that is [3,3,3,3,3].

Array access is just a[0].

If you try to access an index outside the array's length, Rust will panic and throw a runtime error. Apparently, many low-level languages will let you access invalid memory in this situation: good job, Rust.

Functions

Snake case, baby.

fn another_function() {
println!("Another function.");
}

You can define and call your functions in any order in the same file; it doesn't matter to Rust.

Here it is with parameters:

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

Function signatures require type declarations! All arguments must have their types defined.

Note that statements in Rust do not return values. So in Ruby, you might do:

x = y = 2

And in JavaScript you might do:

const x = function() { console.log('Function') };

Well, in Rust, neither of those work. Statements do not return values, but expressions do.

5 + 6 is an expression. Calling a function is an expression. Calling a macro is an expression.

Here's a more complex example of an expression:

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

This part is an expression, which evaluates to 4:

{
let x = 3;
x + 1
}

Thus, we can assign it to y.

Note the tricky thing here: x + 1 does not end in a semicolon! Expressions do not have semicolons. If you add one there, it becomes a statement, and doesn't return a value. Tricky tricky.

To have a function return a value, we need to declare the return type and add an expression to the body:

fn five() -> i32 {
5
}

Another example:

fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
x + 1
}

Note that if we changed x + 1 to x + 1;, we'd get an error. The function no longer returns an i32. The actual error would say mismatched type, since statements evalute to (), an empty tuple, so that would be our return value.

Comments

// Comments are easy

Control flow

The following won't work:

fn main() {
let number = 3;
if number {
println!("number was three");
}
}

Values are not coerced to booleans. Instead, do:

fn main() {
let number = 3;
if number == 3 {
println!("number was three");
}
}

Multiple conditions:

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");
}
}

Since if is an expression, you can assign it to a variable, and do a ternary:

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

... but they must be the same type. The following would throw an error:

let condition = true;
let number = if condition { 5 } else { "six" };

Rust has three types of loops: loop, while, for. Here's how you might print every item in an array:

fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}

A more concise version using for:

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

Just for fun, here's a countdown:

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

Ownership

A unique feature to Rust. It allows Rust to have memory safety without a garbage collector.

Other languages use one of two options for managing memory:

  1. A garbage collector trolls about for memory that is no longer needed.
  2. The programmer explicitly allocates and frees memory.

Refresher on stack & heap

Whether a value is stored on stack or heap matters in Rust. Here's what each means:

Stack stores value in last in, first out. Like a stack of plates. All data must be of fixed size. You push on data and then pop off when you need it.

Heap stores values by address, which is called a pointer. Requesting a certain amount of memory and getting a pointer back is called allocating. To get your data, follow the pointer.

The stack is faster, since you don't have to find a place: you just push it onto the stack. When you call a function, its values are put onto the stack for fast access, and then popped off when you're done.

Managing the heap is the goal of garbage collectors (in other languages) and ownership rules (in Rust). We want to avoid duplicate data and clean up unused data.

Back to ownership

Here are the three rules of ownership:

"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."

Here's an example of scope:

fn main() { // s is not valid here
let s = "hello"; // s becomes valid
// s is valid here
} // scope over, s no longer valid

The above is a string literal, which are immutable. If you want a mutable string, use String:

let string = String::from("hello");

More on Strings later, but first, a question: why is a string literal immutable, and a String mutable?

A string literal is immutable because its size is known at compile time: it becomes hardcoded into the final executable. Since a String is mutable, we need to allocate memory in the heap for it, instead.

That implies allocating the memory, and then freeing it once we're done with our String.

In Rust, that memory is automatically freed when our String goes out of scope. When our variable string goes out of scope, Rust calls a function called drop behind the scenes (this happens at the last curly bracket of the scope). This frees up the memory.

So the variable string "owns" the memory used to store "hello", and once it is out of scope, that memory is free.

Here's an example of allocation with a String:

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

When s1 is created, behind the scenes it is composed of three things:

  1. Length (in this case, 5)
  2. Capacity (also 5)
  3. A pointer to the memory in the heap where it is stored

(Length is how much memory the string is using, capacity is how much has been allocated to it.)

s2 has the same length, capacity, and pointer. No new memory needs to be allocated, which is efficient.

But the downside is that s1 and s2 have the same pointer, so when they are dropped, you would normally get what's called a "double free" error: trying to free the same memory twice.

So to avoid this, Rust does something sneaky: it invalidates s1. If you try to use it like so...

let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);

... you get a "borrow of moved value" error. The pointer has been moved from s1 to s2, so s1 is useless now: it is effectively out of the scope.

If you did want to properly copy s1, and create a new pointer in memory for s2, you can use the clone method:

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

This works just fine.

Now, there are some exceptions to this. Things like integers are of fixed size and thus cheap to copy, so Rust does it automatically. E.g. the below code works fine:

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

Behind the scenes, it does the same thing as:

let x = 5;
let y = x.clone();
println!("x = {}, y = {}", x, y);

Integers possess a trait called Copy that drives this behaviour, which you can add to your own types. Copy types are stored entirely on the stack. Other types that are automatically Copy-ed:

  • all integers
  • booleans
  • floats
  • chars
  • tuples of Copy types

Ownership also means values can "move" into functions:

fn main() {
let s = String::from("hello"); // s is stored in heap
takes_ownership(s); // s's value moves into the function...
// s can no longer be used in this scope
}
fn some_function(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // some_string goes out of scope, is dropped, and memory in heap is freed

Again, it's different with integers and other Copy types:

fn main() {
let x = 5; // x is stored in stack
takes_ownership(x); // x's value moves into the function...
// but since x is Copy, it can still be used here
}
fn some_function(some_number: i32) { // some_number comes into scope
println!("{}", some_string);
} // some_number goes out of scope, but nothing happens

When it comes to functions and return values, it's a little complicated. Here's an annotated example:

fn main() {
let s1 = generates_a_string(); // the return value of this function is moved to s1
} // s1 goes out of scope and is dropped
fn generates_a_string() {
let some_string = String::from("hello"); // some_string comes into scope
some_string // some_string is returned and moves out of scope
}

In the above example, you have a string that is created in one scope, moves to another, and then is dropped.

Here's another example:

fn main() {
let string1 = String::from("hello"); // A string with the value of "hello" is created
let string2 = takes_ownership(string1) // string1 is passed out of scope. The return value of takes_ownership is moved into string2
} // string1 goes out of scope, but it was already moved to string2, so nothing happens. string2 is dropped.
fn takes_and_returns(a_string: String) -> String {
a_string // the string is returned and moved to the calling function
}

Here, we're only dealing with one value: a string that says "hello". That data is attached to a variable called string1, then moved to the second function and called a_string, then moved BACK to the first function and called string2, until it is finally dropped. Ownership is transferred, but the underlying value (and its place in memory) is the same.

Now, you might see a problem here. What if you want to pass a variable to a function, but then use it again? You'd have to be careful to pass ownership back, which could be painful. Here's one example of that painful route using a tuple:

fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}

We pass in the string, get its length, but we have to make sure to get both the string itself and the length back from the function, if we want to use the string again. Blah. Good thing Rust has us covered, with...

References

Here's the really simple fix for the above:

fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}

The & implies that s1 is a reference, which is essentially a temporary loan to the calculate_length function. What is actually happening is that the s parameter in calculate_length has a pointer that points to the s1 variable, which in turn points to the actual value in memory.

Note that you need the &String type in the declaration for calculate_length to be clear it is a reference.

This whole process is called borrowing. Makes sense.

You cannot modify a borrowed value. calculate_length could not change the string. Don't mess with stuff you borrowed. References are immutable. Except when they aren't.

Yes, you can have mutable references.

fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}

Note you have to put mut three times: when we declare the variable, and when we pass it as a reference, and when we declare the function parameter.

But to keep things sane, you can only have one mutable reference per variable in a given scope. This code will cause an error cannot borrow s as mutable more than once at a time:

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

The problem Rust is trying to prevent here is called a data race, where multiple pointers have read/write access to a value; what happens if one writes while the other reads, simultaneously?

A couple more scenarios with mutable/immutable references. This code is okay: we have multiple mutable references, but they're in different scopes:

let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;

This code is bad, as you cannot have both immutable and mutable references to the same variable:

let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3);

So, in short, you have to choose. You can have one or multiple immutable references, OR one mutable reference (per scope).

Let's complicate it a little more. A reference's scope starts from where it is declared, and ends when it is last used. That means you can do this:

let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut s; // no problem
println!("{}", r3);

If we changed this code to use r1 or r2 after the last line, THEN we'd have a problem.

Rust will throw an error if a reference is allowed to persist when the value it points to has gone out of scope. For example:

fn main() {
let reference = get_bad_reference() // We receive a reference
}
fn get_bad_reference() -> &String {
let string = String::from("hello");
&string // We return a reference to string
} // But string goes out of scope here! Its memory is gone.

(To fix this problem, return string itself, not a reference, which will move ownership out of that scope.)

The slice type

If you had the string "hello world", and you only wanted to reference the "world" part, you could use a string slice.

let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];

In this case, the world variable's pointer is to the 7th byte of the string. s variable's pointer is to the 0th byte.

Note that world doesn't point to the s variable; it points to the underlying memory, same as s.

You can drop the numbers that refer to the first and last character, for convenience:

let hello = &s[..5];
let world = &s[6..];

To take the entire string, you can do:

let hello_world = &s[..]

Slices are immutable references. There's an interesting consequence to this. Behold the following code, which will fail:

let mut s = String::from("hello world");
let hello = &s[..5];
s.clear();

To call clear() (which empties the string), Rust needs to create a mutable reference first. But hello is already an immutable reference, and as we learned, you can't have an immutable reference and a mutable reference at the same time. This is good, because it protects hello from trying to access an empty string.

String literals, like the one below, are slices. What does this mean? Well, look at it first:

let s = "Hello, world!";

Since the string is hardcoded into the binary, s is actually a reference. It points to the specific location of the string in the binary. Thus, it is an immutable reference.

You can also slice arrays:

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

For reference, the type of slice is &[i32]. It's a reference to an array of integer values, pointing to a specific byte.

Structs

To make a struct, you define it, then instantiate it:

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

Since in this case the instance is mutable, we can do this:

user1.email = String::from("anotheremail@example.com");

You can't have immutable fields and mutable fields in the same instance: either it's all mutable, or none of it is.

Here's a function that returns an instance:

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

You can also use this shorthand (called field init shorthand):

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

Use the .. syntax to create new instances based on old instances:

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

Tuple structs

Let's start with an example:

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

Color and Point are structs, but they contain values without keys, like tuples. This is allowed. The advantage here is that the type of black is Color. If you had a function that only takes the Color type, you could not pass it a Point type.

Other than their types, tuple structs behave like tuples.

Structs can store references, but this involves the use of lifetimes, something we'll cover later.

To print out a struct, you need to add the debug annotation:

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}

This will yield:

$ cargo run
Compiling structs v0.1.0 (file:///projects/structs)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/structs`
rect1 is Rectangle { width: 30, height: 50 }

If you swapped the {:?} for {:#}, you'd get:

$ cargo run
Compiling structs v0.1.0 (file:///projects/structs)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/structs`
rect1 is Rectangle {
width: 30,
height: 50,
}

Struct method syntax

Methods are functions that are defined within the context of a struct (or enum or trait object); their first parameter is always self, which refers to the instance.

To define a method, you must define the struct and then implement the method:

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

To invoke a method:

rect1.area();

If you wanted your method to mutate the instance, you'd have to specify &mut self as the parameter.

You can also have the method take ownership of self (and not use a reference), but that's rare.

To implement multiple methods, put them in the same impl block:

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
}
}

You can also write this in multiple impl blocks, though it's uglier:

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

Associated functions

You can also do this:

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

... but since square does not receive self as an argument, it's not a method. It's called an associated function, and you invoke it via Rectangle::square(3);. It's like a class method in Ruby.

Enums

Here's how you define an enum:

enum TacoType {
Fish,
Beef,
}

You can then create instances of each variant:

let fish = TacoType::Fish;
let beef = TacoType::Beef;

Both variants are of the same type, which means you can define a function that takes either:

fn place_taco_order(taco_type: TacoType) {
}

You can combine enums with structs:

struct TacoRecipe {
type: TacoType,
name: String,
}
let tuna = TacoRecipe {
type: TacoType::Fish,
name: String::from("Tuna"),
}

Or you can just bundle that data into the enum:

enum Taco {
Fish(String),
Beef(String),
}
let tuna = Taco::Fish(String::form("Tuna"));

You can even have different types for different enum values:

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

Enums can also have methods:

enum Taco {
Fish(String),
Beef(String),
}
impl Taco {
fn order(&self) {
}
}
let taco = Taco::Fish(String::from("Tuna"));
taco.order();

In order(), &self would thus be Taco::Fish(String::from("Tuna")).

A common enum type from the standard library is Option. Rust doesnt have a null. Instead, the Option type encodes the scenario of a value either being present or absent.

enum Option<T> {
Some(T),
None,
}

Here, <T> is the generic type parameter. Here's how you would use it (note you don't have to import Option or its variants!):

let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;

The catch with Option values is that they can be null (that's the point!). But Rust forces you to always check if the value is null before you use it, as a safety mechanism.

For example, this code fails because y is an Option:

let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;

In order for this to work, you need to convert Option to a definite type first. To do this, see the documentation.

unwrap(), for example, returns the Some value of an Option type, but it's discouraged in the docs:

Returns the contained Some value, consuming the self value. Because this function may panic, its use is generally discouraged. Instead, prefer to use pattern matching and handle the None case explicitly, or call unwrap_or, unwrap_or_else, or unwrap_or_default.

So you have to be able to handle the null case. This is where match comes in.

The match operator

Match expressions are essentially switch statements. Here's an enum example:

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,
}
}

The expression is made up of "arms", which are made up of a pattern and some code.

Here's how you'd write a multiline match arm:

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

match expressions can be used to extract values from enums. Let's go back to our tacos.

enum Filling {
Beef,
Fish,
}
enum Taco {
Hard(Filling),
Soft(Filling)
}

We can create a taco with Taco::Hard(Filling::Beef). Here's a match expression which prints the filling of the hard taco:

fn get_filling(taco: Taco) -> u8 {
match coin {
Taco::Soft => "Soft",
Taco::Hard(filling) => {
println!("Filling is {:?}!", filling);
"Hard"
}
}
}

The filling parameter binds to the value of the taco's filling. So we'd get back Filling::Beef.

Here's an example of matching with Option:

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);

none is thus set to None, and six is set to 6.

Rustaceans apparently love the match + enum pattern.

There's one thing about matches: they have to be exhaustive. You can't do this:

fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
}
}

Rust will complain that we didn't handle every variant. We can fix this by adding more match arms, or by adding a wildcard:

let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}

If let

Look at this match expression:

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

If some_u8_value is Some(3), then we print. But we don't care about any other case: not any other Some value, or None.

A more concise way to write it:

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

You can also add an else:

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

Get my monthly reading list

A short, once-a-month email of useful articles & guides: the best of what I write, and the best of what I find.

Sign up now and get a free copy of my mini-guide, "How to Accelerate Your Developer Career".