- 1 Getting Started
- 3 Common Concepts
- 4 Understanding Ownership
- 5 Using Structs to Structure Related Data
- 6 Enums and Pattern Matching
- 8 Common Collections
- 8-1 Storing Lists of Values with Vectors
- 8-2 Storing UTF-8 Encoded Text with Strings
- 8-3 Storing Keys with Associated Values in Hash Maps
- 9 Error Handling
- 10 Generic Types, Traits, and Lifetimes
- 10-1 Generic Data Types
- 10-2 Traits Defining Shared Behavior
- 10-3 Validating References with Lifetimes
- 10-3-1 Preventing Dangling References with Lifetimes
- 10-3-2 The Borrow Checker
- 10-3-3 Generic Lifetimes in Functions
- 10-3-4 Lifetime Annotation Syntax
- 10-3-5 Lifetime Annotations in Function Signatures
- 10-3-6 Thinking in Terms of Lifetimes
- 10-3-7 Lifetime Annotations in Struct Definitions
- 10-3-8 Lifetime Elision
- 10-3-9 Lifetime Annotations in Method Definitions
- 10-3-10 The Static Lifetime
- 10-4 Generic Type Parameters Trait Bounds and Lifetimes Together
- 11 Writing Automated Tests
- 18 Patterns and Matching
- 18-1 All the Places Patterns Can Be Used
- 18-2 Refutability Whether a Pattern Might Fail to Match
- 18-3 Pattern Syntax
1 Getting Started
1 | $ cargo new proj |
3 Common Concepts
1 | // Raw identifiers |
3-1 Variables and Mutability
1 | let mut x = 5; // mutable |
3-1-1 Variables and Constants
mut
is not allowed with constants (always immutable).using the
const
keyword instead oflet
, and 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.
1 | const MAX_POINTS: u32 = 100_000; // 100000 |
Constants are valid for the entire time a program runs, within the scope they were declared in.
3-1-2 Shadowing
1 | fn main() { |
By using let
, we can perform a few transformations on a value but have the variable be immutable after those transformations have been completed.
The other difference between mut
and shadowing is that because we’re effectively creating a new variable when we use the let
keyword again, we can change the type of the value but reuse the same name.
1 | let spaces = " "; |
3-2 Data Types
Rust is a statically typed language, which means that it must know the types of all variables at compile time.
1 | let guess: u32 = "42".parse().expect("Not a number!"); |
3-2-1 Scalar
A scalar type represents a single value.
3-2-1-1 Integer
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 |
u8 |
16-bit | i16 |
u16 |
32-bit | i32 |
u32 |
64-bit | i64 |
u64 |
128-bit | i128 |
u128 |
arch | isize |
usize |
Each signed variant can store numbers from -(2^{n - 1})
to 2^{n - 1} - 1
inclusive, where n is the number of bits that variant uses. Unsigned variants can store numbers from 0 to 2^{n - 1}
.
The isize
and usize
types depend on the kind of computer your program is running on.
3-2-1-2 Numeric
Can not operate two different types of nums.
3-2-1-3 Boolean
1 | fn main() { |
3-2-1-4 Character
1 | fn main() { |
Rust’s char
type represents a Unicode Scalar Value.
3-2-2 Compound Types
3-2-2-1 Tuple
1 | fn main() { |
3-2-2-2 Array
Unlike a tuple, every element of an array must have the same type. Arrays have a fixed length, like tuples.
1 | fn main() { |
A vector is a similar collection type provided by the standard library that is allowed to grow or shrink in size. If not sure, use a vector.
1 | fn main() { |
The first (i32) is the type of each element. Since all elements have the same type, just list it once.
After the semicolon, there’s a number that indicates the length of the array.
3-3 Functions
3-3-1 Parameters
1 | fn main() { |
must declare the type of each parameter.
3-3-2 Statements and Expressions
Cannot write x = y = 6
.
1 | fn main() { |
That value gets bound to y
as part of the let
statement. Note the x + 1
line without ;
at the end.
Expressions do not include ;
. If added it’s a statement, which will then not return a value.
3-3-3 Return Values
Declare their type after an arrow (->
). In Rust, the return value of the function is synonymous with the value of the final expression in the block of the body of a function.
You can return early from a function by using the return
keyword and specifying a value, but most functions return the last expression implicitly.
1 | fn five() -> i32 { |
3-4 Control Flow
3.4.1 if
1 | fn main() { |
The condition in this code must be a bool
.
Rust only executes the block for the first true condition, and once it finds one, it doesn’t even check the rest.
Using too many else if
expressions can clutter the code, so we use match
.
1 | fn main() { |
3-4-2 Repetition
3-4-2-1 loop
1 | fn main() { |
3-4-2-2 while
1 | fn main() { |
3-4-2-3 for
1 | fn main() { |
This approach is error prone; we could cause the program to panic if the index length is incorrect. It’s also slow, because the compiler adds runtime code to perform the conditional check on every element on every iteration through the loop.
As a more concise alternative, you can use a for
loop and execute some code for each item in a collection.
1 | fn main() { |
4 Understanding Ownership
4-1 What Is Ownership?
Rust uses a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks at compile time. None of the ownership features slow down your program while it’s running.
堆和栈:
- 栈:速度快(最上面的那个),大小固定;函数调用。
- 堆:速度慢(指派空间,指针跳转);减少堆上数据复制,及时清理未使用的数据。
4-1-1 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.
4-1-2 Variable Scope
1 | { // s is not valid here, it’s not yet declared |
4-1-3 The String Type
1 | let mut s = String::from("hello"); |
4-1-4 Memory and Allocation
With the String
type, in order to support a mutable, growable piece of text, we need to allocate an amount of memory on the heap, unknown at compile time, to hold the contents. This means:
- The memory must be requested from the operating system at runtime: when we call
String::from
, its implementation requests the memory it needs. - Returning this memory to the operating system when we’re done with our
String
: the memory is automatically returned (use a function calleddrop
) once the variable that owns it goes out of scope.
4-1-4-1 Move
1 | let s1 = String::from("hello"); |
A String
is made up of three parts:
- a pointer to the memory that holds the contents of the string
- a length
- a capacity.
This group of data is stored on the stack. On the right is the memory on the heap that holds the contents.
The length is how much memory, in bytes, the contents of the String
is currently using. The capacity is the total amount of memory, in bytes, that the String
has received from the operating system.
When we assign s1
to s2
, the String
data is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. We do not copy the data on the heap that the pointer refers to.
Both data pointers pointing to the same location. This is a problem: when s2
and s1
go out of scope, they will both try to free the same memory. This is known as a double free error and is one of the memory safety bugs. Freeing memory twice can lead to memory corruption, which can potentially lead to security vulnerabilities.
Instead of trying to copy the allocated memory, Rust let s1 invalid, so that we do not need to care s1 any more. That’s a bit like “shallow copy”, but in Rust we call it “move”.
1 | let s1 = String::from("hello"); |
In addition, there’s a design choice that’s implied by this: Rust will never automatically create “deep” copies of your data. Therefore, any automatic copying can be assumed to be inexpensive in terms of runtime performance.
4-1-4-2 Clone
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"); |
4-1-4-3 Copy
1 | let x = 5; |
Here, we don’t have a call to clone
, but x
is still valid and wasn’t moved into y
.
The reason is that types such as integers that have a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make. There’s no difference between deep and shallow copying here, so calling clone
wouldn’t do anything different from the usual shallow copying and we can leave it out.
As a general rule, any group of simple scalar values can be Copy
, and nothing that requires allocation or is some form of resource is Copy
.
- All the integer types, such as
u32
. - The Boolean type,
bool
, with valuestrue
andfalse
. - All the floating point types, such as
f64
. - The character type,
char
. - Tuples, if they only contain types that are also
Copy
. For example,(i32, i32)
isCopy
, but(i32, String)
is not.
4-1-5 Ownership and Functions
The semantics for passing a value to a function are similar to those for assigning a value to a variable. Passing a variable to a function will move or copy, just as assignment does.
1 | fn main() { |
4-1-6 Return Values and Scope
Returning values can also transfer ownership.
1 | fn main() { |
It’s possible to return multiple values using a tuple.
1 | fn main() { |
But this is too much ceremony and a lot of work for a concept that should be common. Luckily for us, Rust has a feature for this concept, called references.
4-2 References and Borrowing
1 | fn main() { |
First, notice that all the tuple code in the variable declaration and the function return value is gone. Second, note that we pass &s1
into calculate_length
and, in its definition, we take &String
rather than String
. These ampersands are references, and they allow you to refer to some value without taking ownership of it.
The &s1
syntax lets us create a reference that refers to the value of s1
but does not own it. We call having references as function parameters borrowing.
1 | fn main() { |
Just as variables are immutable by default, so are references. We’re not allowed to modify something we have a reference to.
4-2-1 Mutable References
1 | fn main() { |
First, we had to change s
to be mut
. Then we had to create a mutable reference with &mut s
and accept a mutable reference with some_string: &mut String
.
But mutable references have one big restriction: you can have only one mutable reference to a particular piece of data in a particular scope.
1 | let mut s = String::from("hello"); |
The benefit of having this restriction is that Rust can prevent data races at compile time. A data raceis similar to a race condition and happens when these three behaviors occur:
- Two or more pointers access the same data at the same time.
- At least one of the pointers is being used to write to the data.
- There’s no mechanism being used to synchronize access to the data.
Data races cause undefined behavior and can be difficult to diagnose and fix when you’re trying to track them down at runtime; Rust prevents this problem from happening because it won’t even compile code with data races!
As always, we can use curly brackets to create a new scope, allowing for multiple mutable references, just not simultaneous ones:
1 | let mut s = String::from("hello"); |
A similar rule exists for combining mutable and immutable references. This code results in an error:
1 | let mut s = String::from("hello"); |
We also cannot have a mutable reference while we have an immutable one. However, multiple immutable references are okay because no one who is just reading the data has the ability to affect anyone else’s reading of the data.
4-2-2 Dangling References
1 | fn main() { |
Because s
is created inside dangle
, when the code of dangle
is finished, s
will be deallocated. But we tried to return a reference to it. That means this reference would be pointing to an invalid String
.
4-3 The Slice Type
1 | fn first_word(s: &String) -> usize { |
4-3-1 String Slices
1 | let s = String::from("hello world"); |
The type that signifies “string slice” is written as &str
1 | fn first_word(s: &String) -> &str { |
Recall from the borrowing rules that if we have an immutable reference to something, we cannot also take a mutable reference. Because clear
needs to truncate the String
, it tries to take a mutable reference, which fails.
1 | fn first_word(s: &mut String) -> &str { |
4-3-1-1 String Literals Are Slices
1 | let s = "Hello, world!"; |
The type of s
here is &str
: it’s a slice pointing to that specific point of the binary. This is also why string literals are immutable; &str
is an immutable reference.
4-3-1-2 String Slices as Parameters
1 | fn first_word(s: &str) -> &str { |
4-3-2 Other Slices
1 | let a = [1, 2, 3, 4, 5]; |
This slice has the type &[i32]
. It works the same way as string slices do, by storing a reference to the first element and a length.
5 Using Structs to Structure Related Data
5-1 Defining and Instantiating Structs
1 | struct User { |
Note that the entire instance must be mutable; Rust doesn’t allow us to mark only certain fields as mutable. As with any expression, we can construct a new instance of the struct as the last expression in the function body to implicitly return that new instance.
1 | fn build_user(email: String, username: String) -> User { |
5-1-1 Using the Field Init Shorthand When Variables and Fields Have the Same Name
1 | fn build_user(email: String, username: String) -> User { |
5-1-2 Creating Instances From Other Instances With Struct Update Syntax
1 | let user2 = User { |
The syntax ..
specifies that the remaining fields not explicitly set should have the same value as the fields in the given instance.
5-1-3 Using Tuple Structs without Named Fields to Create Different Types
Structs that look similar to tuples, called tuple structs.
typedef int myint;
to define a meaningful data type.
1 | struct Color(i32, i32, i32); |
Each struct you define is its own type, even though the fields within the struct have the same types. Otherwise, tuple struct instances behave like tuples: you can destructure them into their individual pieces, you can use a .
followed by the index to access an individual value, and so on.
5-1-4 Unit-Like Structs Without Any Fields
Unit-like structs can be useful in situations in which you need to implement a trait on some type but don’t have any data that you want to store in the type itself.
5-1-5 Ownership of Struct Data
1 | struct User { |
5-2 An Example Program Using Structs
1 | fn main() { |
5-2-1 Refactoring with Tuples
1 | fn main() { |
5-2-2 Refactoring with Structs Adding More Meaning
1 | struct Rectangle { |
5-2-3 Adding Useful Functionality with Derived Traits
1 |
|
5-3 Method Syntax
Methods 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.
5-3-1 Defining Methods
1 |
|
Rust knows the type of self
is Rectangle
due to this method’s being inside the impl Rectangle
context. Methods can take ownership of self
, borrow self
immutably as we’ve done here, or borrow self
mutably, just as they can any other parameter.
We’ve chosen &self
here for the same reason we used &Rectangle
in the function version: we don’t want to take ownership, and we just want to read the data in the struct, not write to it. If we wanted to change the instance that we’ve called the method on as part of what the method does, we’d use &mut self
as the first parameter.
5-3-2 Methods with More Parameters
1 | impl Rectangle { |
5-3-3 Associated Functions
Another useful feature of impl
blocks is that we’re allowed to define functions within impl
blocks that don’t take self
as a parameter. These are called associated functions because they’re associated with the struct. They’re still functions, not methods, because they don’t have an instance of the struct to work with. You’ve already used the String::from
associated function.
Associated functions are often used for constructors that will return a new instance of the struct.
1 | impl Rectangle { |
5-3-4 Multiple impl Blocks
1 | // just like |
6 Enums and Pattern Matching
6-1 Defining an Enum
That property of IP addresses makes the enum data structure appropriate, because enum values can only be one of the variants.
1 | enum IpAddrKind { |
6-1-1 Enum Values
1 | let four = IpAddrKind::V4; |
1 | struct IpAddr { |
1 | enum IpAddr { |
There’s another advantage to using an enum rather than a struct: each variant can have different types and amounts of associated data.
1 | enum IpAddr { |
This code illustrates that you can put any kind of data inside an enum variant: strings, numeric types, or structs, for example. You can even include another enum!
1 | struct Ipv4Addr { |
1 | enum Message { |
6-1-2 The Option Enum and Its Advantages Over Null Vlues
The problem isn’t really with the concept but with the particular implementation. As such, Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent.
1 | enum Option<T> { |
If we use None
rather than Some
, we need to tell Rust what type of Option<T>
we have, because the compiler can’t infer the type that the Some
variant will hold by looking only at a None
value.
So why is having Option<T>
any better than having null? In short, because Option<T>
and T
(where T
can be any type) are different types, the compiler won’t let us use an Option<T>
value as if it were definitely a valid value.
1 | let x: i8 = 5; |
6-2 The match Control Flow Operator
1 | enum Coin { |
6-2-1 Patterns that Bind to Values
1 | // so we can inspect the state in a minute |
6-2-2 Matching with Option<T>
1 | fn plus_one(x: Option<i32>) -> Option<i32> { |
Combining match
and enums is useful in many situations. You’ll see this pattern a lot in Rust code: match
against an enum, bind a variable to the data inside, and then execute code based on it.
6-2-3 Matches Are Exhaustive
Rust knows that we didn’t cover every possible case and even knows which pattern we forgot! Matches in Rust are exhaustive: we must exhaust every last possibility in order for the code to be valid. Especially in the case of Option<T>
, when Rust prevents us from forgetting to explicitly handle the None
case, it protects us from assuming that we have a value when we might have null, thus making the billion-dollar mistake discussed earlier.
6-2-4 The _
Placeholder
1 | let some_u8_value = 0u8; |
The _
will match all the possible cases that aren’t specified before it. The ()
is just the unit value, so nothing will happen in the _
case.
6-3 Concise Control Flow with if let
1 | let some_u8_value = Some(0u8); |
Using if let
means less typing, less indentation, and less boilerplate code. However, you lose the exhaustive checking that match
enforces. Choosing between match
and if let
depends on what you’re doing in your particular situation and whether gaining conciseness is an appropriate trade-off for losing exhaustive checking.
1 | let mut count = 0; |
8 Common Collections
Unlike the built-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.
8-1 Storing Lists of Values with Vectors
Vectors can only store values of the same type.
8-1-1 Create a New Vector
1 | let v: Vec<i32> = Vec::new(); |
It’s more common to create a Vec<T>
that has initial values, and Rust provides the vec!
macro for convenience.
8-1-2 Updating a Vector
1 | let mut v = Vec::new(); |
The numbers we place inside are all of type i32
, and Rust infers this from the data, so we don’t need the Vec<i32>
annotation.
8-1-3 Dropping a Vector Drops Its Elements
1 | { |
8-1-4 Reading Elements of Vectors
Both methods of accessing a value in a vector, either with indexing syntax or the get
method.
1 | let v = vec![1, 2, 3, 4, 5]; |
Why should a reference to the first element care about what changes at the end of the vector? This error is due to the way vectors work: adding a new element onto the end of the vector might require allocating new memory and copying the old elements to the new space, if there isn’t enough room to put all the elements next to each other where the vector currently is. In that case, the reference to the first element would be pointing to deallocated memory. The borrowing rules prevent programs from ending up in that situation.
8-1-5 Iterating over the Values in a Vector
1 | let v = vec![100, 32, 57]; |
8-1-6 Using an Enum to Store Multiple Types
1 | enum SpreadsheetCell { |
Rust needs to know what types will be in the vector at compile time so it knows exactly how much memory on the heap will be needed to store each element.
8-2 Storing UTF-8 Encoded Text with Strings
8-2-1 What Is a String?
Rust’s standard library also includes a number of other string types, such as OsString
, OsStr
, CString
, and CStr
. These string types can store text in different encodings or be represented in memory in a different way.
8-2-2 Creating a New String
1 | let mut s = String::new(); |
8-2-3 Updating a String
8-2-3-1 Appending to a String with push_str
and push
1 | let mut s = String::from("foo"); |
The push_str
method takes a string slice because we don’t necessarily want to take ownership of the parameter.
8-2-3-2 Concatenation with the +
Operator or the format!
Macro
1 | let s1 = String::from("Hello, "); |
First, s2
has an &
, meaning that we’re adding a reference of the second string to the first string because of the s
parameter in the add
function: we can only add a &str
to a String
; we can’t add two String
values together. But wait—the type of &s2
is &String
, not &str
, as specified in the second parameter to add
. So why does it compile?
The reason we’re able to use &s2
in the call to add
is that the compiler can coerce the &String
argument into a &str
. When we call the add
method, Rust uses a deref coercion, which here turns &s2
into &s2[..]
. Because add
does not take ownership of the s
parameter, s2
will still be a valid String
after this operation.
Second, we can see in the signature that add
takes ownership of self
, because self
does not have an &
. This means s1
will be moved into the add
call and no longer be valid after that. So although let s3 = s1 + &s2;
looks like it will copy both strings and create a new one, this statement actually takes ownership of s1
, appends a copy of the contents of s2
, and then returns ownership of the result. In other words, it looks like it’s making a lot of copies but isn’t; the implementation is more efficient than copying.
1 | let s1 = String::from("tic"); |
8-2-4 Indexing into Strings
Rust strings don’t support indexing.
1 | let s1 = String::from("hello"); |
8-2-4-1 Internal Representation
1 | let hello = "Здравствуйте"; |
8-2-4-2 Bytes and Scalar Values and Grapheme Clusters
Another point about UTF-8 is that there are actually three relevant ways to look at strings from Rust’s perspective: as bytes, scalar values, and grapheme clusters (the closest thing to what we would call letters).
Rust provides different ways of interpreting the raw string data that computers store so that each program can choose the interpretation it needs, no matter what human language the data is in.
8-2-5 Slicing Strings
Indexing into a string is often a bad idea because it’s not clear what the return type of the string-indexing operation should be: a byte value, a character, a grapheme cluster, or a string slice. Therefore, Rust asks you to be more specific if you really need to use indices to create string slices. To be more specific in your indexing and indicate that you want a string slice, rather than indexing using []
with a single number, you can use []
with a range to create a string slice containing particular bytes:
1 | let hello = "Здравствуйте"; |
8-2-6 Methods for Iterating Over Strings
If you need to perform operations on individual Unicode scalar values, the best way to do so is to use the chars
method.
1 | for c in "नमस्ते".chars() { |
8-2-7 Exercise
1 | fn string_slice(arg: &str) { println!("{}", arg); } |
8-3 Storing Keys with Associated Values in Hash Maps
8-3-1 Creating a New Hash Map
1 | use std::collections::HashMap; |
Just like vectors, hash maps store their data on the heap. Like vectors, hash maps are homogeneous: all of the keys must have the same type, and all of the values must have the same type.
The type annotation HashMap<_, _>
is needed here because it’s possible to collect
into many different data structures and Rust doesn’t know which you want unless you specify. For the parameters for the key and value types, however, we use underscores, and Rust can infer the types that the hash map contains based on the types of the data in the vectors.
8-3-2 Hash Maps and Ownership
1 | let field_name = String::from("Favorite color"); |
We aren’t able to use the variables field_name
and field_value
after they’ve been moved into the hash map with the call to insert
.
If we insert references to values into the hash map, the values won’t be moved into the hash map. The values that the references point to must be valid for at least as long as the hash map is valid.
8-3-3 Accessing Values in a Hash Map
1 | let team_name = String::from("Blue"); |
score
will have the value that’s associated with the Blue team, and the result will be Some(&10)
. The result is wrapped in Some
because get
returns an Option<&V>
; if there’s no value for that key in the hash map, get
will return None
.
1 | for (key, value) in &scores { |
8-3-4 Updating a Hash Map
8-3-4-1 Overwriting a Value
1 | scores.insert(String::from("Blue"), 10); |
8-3-4-2 Only Inserting a Value If the Key Has No Value
1 | scores.entry(String::from("Yellow")).or_insert(50); |
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. This technique is much cleaner than writing the logic ourselves and, in addition, plays more nicely with the borrow checker.
8-3-4-3 Updating a Value Based on the Old Value
1 | use std::collections::HashMap; |
The or_insert
method actually returns a mutable reference (&mut V
) to the value for this key.
8-3-5 Hashing Functions
By default, HashMap
uses a “cryptographically strong”1 hashing function that can provide resistance to Denial of Service (DoS) attacks. This is not the fastest hashing algorithm available, but the trade-off for better security that comes with the drop in performance is worth it. If you profile your code and find that the default hash function is too slow for your purposes, you can switch to another function by specifying a different hasher. A hasher is a type that implements the BuildHasher
trait.
9 Error Handling
Rust doesn’t have exceptions. Instead, it 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. This most commonly occurs when a bug of some kind has been detected and it’s not clear to the programmer how to handle the error.
By default, when a panic occurs, the program starts unwinding, which means Rust walks back up the stack and cleans up the data from each function it encounters. But this walking back and cleanup is a lot of work. The alternative is to immediately abort, which ends the program without cleaning up. Memory that the program was using will then need to be cleaned up by the operating system. If in your project you need to make the resulting binary as small as possible, you can switch from unwinding to aborting upon a panic by adding
panic = 'abort'
to the appropriate[profile]
sections in your Cargo.toml file. For example, if you want to abort on panic in release mode, add this:
1
2
3 >[profile.release]
>panic = 'abort'
>
1 | fn main() { |
9-1-1 Using a panic!
Backtrace
1 | fn main() { |
Other languages, like C, will attempt to give you exactly what you asked for in this situation, even though it isn’t what you want: you’ll get whatever is at the location in memory that would correspond to that element in the vector, even though the memory doesn’t belong to the vector. This is called a buffer overread and can lead to security vulnerabilities if an attacker is able to manipulate the index in such a way as to read data they shouldn’t be allowed to that is stored after the array.
To protect your program from this sort of vulnerability, if you try to read an element at an index that doesn’t exist, Rust will stop execution and refuse to continue.
This error points at a file we didn’t write, vec.rs. That’s the implementation of Vec<T>
in the standard library. The code that gets run when we use []
on our vector v
is in vec.rs, and that is where the panic!
is actually happening.
The next note line tells us that we can set the RUST_BACKTRACE
environment variable to get a backtrace of exactly what happened to cause the error. A backtrace is a list of all the functions that have been called to get to this point. Backtraces in Rust work as they do in other languages: the key to reading the backtrace is to start from the top and read until you see files you wrote. That’s the spot where the problem originated. The lines above the lines mentioning your files are code that your code called; the lines below are code that called your code. These lines might include core Rust code, standard library code, or crates that you’re using. Let’s try getting a backtrace by setting the RUST_BACKTRACE
environment variable to any value except 0.
RUST_BACKTRACE=1 cargo run
Debug symbols are enabled by default when using cargo build
or cargo run
without the --release
flag.
9-2 Recoverable Errors with Result
1 | use std::fs::File; |
The return type of the File::open
function is a Result<T, E>
. The generic parameter T
has been filled in here with the type of the success value, std::fs::File
, which is a file handle. The type of E
used in the error value is std::io::Error
.
9-2-1 Matching on Different Errors
1 | use std::fs::File; |
A more seasoned Rustacean might write this code:
1 | use std::fs::File; |
9-2-2 Shortcuts for Panic on Error unwrap
and expect
unwrap
is a shortcut method that is implemented just like the match
expression, 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 for us.
1 | use std::fs::File; |
expect
, which is similar to unwrap
, lets us also choose the panic!
error message. Using expect
instead of unwrap
and providing good error messages can convey your intent and make tracking down the source of a panic easier.
1 | use std::fs::File; |
9-2-3 Propagating Errors
1 | use std::io; |
9-2-4 A Shortcut for Propagating Errors the ?
Operator
1 | use std::io; |
9-2-5 The ?
Operator Can Only Be Used in Functions That Return Result
Because it is defined to work in the same way as the match
expression.
1 | use std::fs::File; |
However, we can change how we write the main
function so that it does return a Result<T, E>
:
1 | use std::error::Error; |
For now, you can read Box<dyn Error>
to mean “any kind of error.”
9-3 To panic!
or Not to panic!
When you choose to return a Result
value, you give the calling code options rather than making the decision for it. The calling code could choose to attempt to recover in a way that’s appropriate for its situation, or it could decide that an Err
value in this case is unrecoverable, so it can call panic!
and turn your recoverable error into an unrecoverable one. Therefore, returning Result
is a good default choice when you’re defining a function that might fail. In rare situations, it’s more appropriate to write code that panics instead of returning a Result
.
9-3-1 Examples Prototype Code and Tests
If a method call fails in a test, you’d want the whole test to fail, even if that method isn’t the functionality under test. Because panic!
is how a test is marked as a failure, calling unwrap
or expect
is exactly what should happen.
9-3-2 Cases in Which You Have More Information Than the Compiler
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. You’ll still have a Result
value that you need to handle: whatever operation you’re calling still has the possibility of failing in general, even though it’s logically impossible in your particular situation. If you can ensure by manually inspecting the code that you’ll never have an Err
variant, it’s perfectly acceptable to call unwrap
. Here’s an example:
1 | use std::net::IpAddr; |
We’re creating an IpAddr
instance by parsing a hardcoded string. We can see that 127.0.0.1
is a valid IP address, so it’s acceptable to use unwrap
here. However, having a hardcoded, valid string doesn’t change the return type of the parse
method: we still get a Result
value, and the compiler will still make us handle the Result
as if the Err
variant is a possibility because the compiler isn’t smart enough to see that this string is always a valid IP address. If the IP address string came from a user rather than being hardcoded into the program and therefore did have a possibility of failure, we’d definitely want to handle the Result
in a more robust way instead.
9-3-3 Guidelines for Error Handling
It’s advisable to have your code panic when it’s possible that your code could end up in a bad state. In this context, a bad state is when some assumption, guarantee, contract, or invariant has been broken, such as when invalid values, contradictory values, or missing values are passed to your code—plus one or more of the following:
- 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.
However, when failure is expected, it’s more appropriate to return a Result
than to make a panic!
call. Examples include a parser being given malformed data or an HTTP request returning a status that indicates you have hit a rate limit. In these cases, returning a Result
indicates that failure is an expected possibility that the calling code must decide how to handle.
When your code performs operations on values, your code should verify the values are valid first and panic if the values aren’t valid. This is mostly for safety reasons: attempting to operate on invalid data can expose your code to vulnerabilities. This is the main reason the standard library will call panic!
if you attempt an out-of-bounds memory access: trying to access memory that doesn’t belong to the current data structure is a common security problem. Functions often have contracts: their behavior is only guaranteed if the inputs meet particular requirements. Panicking when the contract is violated makes sense because a contract violation always indicates a caller-side bug and it’s not a kind of error you want the calling code to have to explicitly handle. In fact, there’s no reasonable way for calling code to recover; the calling programmers need to fix the code. Contracts for a function, especially when a violation will cause a panic, should be explained in the API documentation for the function.
However, having lots of error checks in all of your functions would be verbose and annoying. Fortunately, you can use Rust’s type system (and thus the type checking the compiler does) to do many of the checks for you. If your function has a particular type as a parameter, you can proceed with your code’s logic knowing that the compiler has already ensured you have a valid value. For example, if you have a type rather than an Option
, your program expects to have something rather than nothing. Your code then doesn’t have to handle two cases for the Some
and None
variants: it will only have one case for definitely having a value. Code trying to pass nothing to your function won’t even compile, so your function doesn’t have to check for that case at runtime. Another example is using an unsigned integer type such as u32
, which ensures the parameter is never negative.
9-3-4 Creating Custom Types for Validation
1 | loop { |
However, this is not an ideal solution: if it was absolutely critical that the program only operated on values between 1 and 100, and it had many functions with this requirement, having a check like this in every function would be tedious (and might impact performance).
Instead, we can make a new type and put the validations in a function to create an instance of the type rather than repeating the validations everywhere. That way, it’s safe for functions to use the new type in their signatures and confidently use the values they receive.
1 | pub struct Guess { |
The panic!
macro signals that your program is in a state it can’t handle and lets you tell the process to stop instead of trying to proceed with invalid or incorrect values. The Result
enum uses Rust’s type system to indicate that operations might fail in a way that your code could recover from.
10 Generic Types, Traits, and Lifetimes
Generics are abstract stand-ins for concrete types or other properties.
10-1 Generic Data Types
10-1-1 In Function Definitions
1 | fn largest_i32(list: &[i32]) -> i32 { |
10-1-2 In Struct Definitions
1 | // x, y same type |
10-1-3 In Enum Definitions
1 | enum Option<T> { |
10-1-4 In Method Definitions
1 | struct Point<T> { |
Generic type parameters in a struct definition aren’t always the same as those you use in that struct’s method signatures.
1 | struct Point<T, U> { |
Here, the generic parameters T
and U
are declared after impl
, because they go with the struct definition. The generic parameters V
and W
are declared after fn mixup
, because they’re only relevant to the method.
10-1-5 Performance of Code Using Generics
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 monomorphized version of the code looks like the following.
1 | enum Option_i32 { |
Because Rust compiles generic code into code that specifies the type in each instance, we pay no runtime cost for using generics. When the code runs, it performs just as it would if we had duplicated each definition by hand. The process of monomorphization makes Rust’s generics extremely efficient at runtime.
10-2 Traits Defining Shared Behavior
A trait tells the Rust compiler about functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic can be any type that has certain behavior.
10-2-1 Defining a Trait
Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose.
1 | pub trait Summary { |
A trait can have multiple methods in its body: the method signatures are listed one per line and each line ends in a semicolon.
10-2-2 Implementing a Trait on a Type
1 | pub struct NewsArticle { |
After implementing the trait, we can call the methods on instances of NewsArticle
and Tweet
in the same way we call regular methods, like this:
1 | let tweet = Tweet { |
use aggregator::Summary;
would enable them to implement Summary
for their type. The Summary
trait would also need to be a public trait for another crate to implement it.
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. For example, we can implement standard library traits like Display
on a custom type like Tweet
as part of our aggregator
crate functionality, because the type Tweet
is local to our aggregator
crate. We can also implement Summary
on Vec<T>
in our aggregator
crate, because the trait Summary
is local to our aggregator
crate.
But we can’t implement external traits on external types. For example, we can’t implement the Display
trait on Vec<T>
within our aggregator
crate, because Display
and Vec<T>
are defined in the standard library and aren’t local to our aggregator
crate. This restriction is part of a property of programs called coherence, and more specifically the orphan rule, so named because the parent type is not present. This rule ensures that other people’s code can’t break your code and vice versa. Without the rule, two crates could implement the same trait for the same type, and Rust wouldn’t know which implementation to use.
10-2-3 Default Implementations
1 | pub trait Summary { |
1 | pub trait Summary { |
To use this version of Summary
, we only need to define summarize_author
when we implement the trait on a type:
1 | impl Summary for Tweet { |
Note that it isn’t possible to call the default implementation from an overriding implementation of that same method.
10-2-4 Traits as Parameters
We can define a function notify
that calls the summarize
method on its parameter item
, which is of some type that implements the Summary
trait. To do this, we can use the impl Trait
syntax, like this:
1 | pub fn notify(item: impl Summary) { |
10-2-4-1 Trait Bound Syntax
1 | pub fn notify<T: Summary>(item: T) { |
It is equivalent to the example above, but is a bit more verbose.
1 | // have two parameters that implement Summary |
10-2-4-2 Specifying Multiple Trait Bounds with +
Syntax
1 | pub fn notify(item: impl Summary + Display) {} |
10-2-4-3 Clearer Trait Bounds with where
Clauses
1 | fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {} |
10-2-5 Returning Types that Implement Traits
1 | fn returns_summarizable() -> impl Summary { |
Using impl Trait
is only allowed if you have a single type that you’re returning.
10-2-6 Fixing the largest
Function with Trait Bounds
1 | fn largest<T>(list: &[T]) -> T { |
In the body of largest
we wanted to compare two values of type T
using the greater than (>
) operator. Because that operator is defined as a default method on the standard library trait std::cmp::PartialOrd
, we need to specify PartialOrd
in the trait bounds for T
so the largest
function can work on slices of any type that we can compare. We don’t need to bring PartialOrd
into scope because it’s in the prelude.
1 | fn largest<T: PartialOrd>(list: &[T]) -> T {} |
Types like i32
and char
that have a known size can be stored on the stack, so they implement the Copy
trait. But when we made the largest
function generic, it became possible for the list
parameter to have types in it that don’t implement the Copy
trait. Consequently, we wouldn’t be able to move the value out of list[0]
and into the largest
variable, resulting in this error.
1 | fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { |
If we don’t want to restrict the largest
function to the types that implement the Copy
trait, we could specify that T
has the trait bound Clone
instead of Copy
. Then we could clone each value in the slice when we want the largest
function to have ownership. Using the clone
function means we’re potentially making more heap allocations in the case of types that own heap data like String
, and heap allocations can be slow if we’re working with large amounts of data.
Another way we could implement largest
is for the function to return a reference to a T
value in the slice. If we change the return type to &T
instead of T
, thereby changing the body of the function to return a reference, we wouldn’t need the Clone
or Copy
trait bounds and we could avoid heap allocations.
10-2-7 Using Trait Bounds to Conditionally Implement Methods
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. For example, the type Pair<T>
always implements the new
function. But Pair<T>
only implements the cmp_display
method if its inner type T
implements the PartialOrd
trait that enables comparison and the Display
trait that enables printing.
1 | use std::fmt::Display; |
We can also conditionally implement a trait for any type that implements another trait. Implementations of a trait on any type that satisfies the trait bounds are called blanket implementations and are extensively used in the Rust standard library. For example, the standard library implements the ToString
trait on any type that implements the Display
trait. The impl
block in the standard library looks similar to this code:
1 | impl<T: Display> ToString for T { |
Because the standard library has this blanket implementation, we can call the to_string method defined by the ToString trait on any type that implements the Display trait. For example, we can turn integers into their corresponding String values like this because integers implement Display:
1 | let s = 3.to_string(); |
10-3 Validating References with Lifetimes
10-3-1 Preventing Dangling References with Lifetimes
x out of scope doesn’t live long enough.
1 | { |
This code won’t compile because the value r
is referring to has gone out of scope before we try to use it.
10-3-2 The Borrow Checker
1 | { |
Here, x
has the lifetime 'b
, which in this case is larger than 'a
. This means r
can reference x
because Rust knows that the reference in r
will always be valid while x
is valid.
10-3-3 Generic Lifetimes in Functions
1 | fn longest(x: &str, y: &str) -> &str { |
When we’re defining this function, we don’t know the concrete values that will be passed into this function, so we don’t know whether the if
case or the else
case will execute. We also don’t know the concrete lifetimes of the references that will be passed in, so we can’t look at the scopes to determine whether the reference we return will always be valid.
10-3-4 Lifetime Annotation Syntax
Lifetime annotations don’t change how long any of the references live. Just as functions can accept any type when the signature specifies a generic type parameter, functions can accept references with any lifetime by specifying a generic lifetime parameter. Lifetime annotations describe the relationships of the lifetimes of multiple references to each other without affecting the lifetimes.
1 | &i32 // a reference |
10-3-5 Lifetime Annotations in Function Signatures
All the references in the parameters and the return value must have the same lifetime.
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
Remember, when we specify the lifetime parameters in this function signature, we’re not changing the lifetimes of any values passed in or returned. Rather, we’re specifying that the borrow checker should reject any values that don’t adhere to these constraints. Note that the longest
function doesn’t need to know exactly how long x
and y
will live, only that some scope can be substituted for 'a
that will satisfy this signature.
1 | fn main() { |
The error shows that for result
to be valid for the println!
statement, string2
would need to be valid until the end of the outer scope. Rust knows this because we annotated the lifetimes of the function parameters and return values using the same lifetime parameter 'a
.
As humans, we can look at this code and see that string1
is longer than string2
and therefore result
will contain a reference to string1
. Because string1
has not gone out of scope yet, a reference to string1
will still be valid for the println!
statement. However, the compiler can’t see that the reference is valid in this case. We’ve told Rust that the lifetime of the reference returned by the longest
function is the same as the smaller of the lifetimes of the references passed in.
10-3-6 Thinking in Terms of Lifetimes
1 | fn longest<'a>(x: &str, y: &str) -> &'a str { |
The problem is that result
goes out of scope and gets cleaned up at the end of the longest
function. We’re also trying to return a reference to result
from the function.
10-3-7 Lifetime Annotations in Struct Definitions
1 | struct ImportantExcerpt<'a> { |
10-3-8 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 rule applies to input lifetimes, and the second and third rules apply to output lifetimes. If the compiler gets to the end of the three rules and there are still references for which it can’t figure out lifetimes, the compiler will stop with an error. These rules apply to fn
definitions as well as impl
blocks.
- The first rule is that each parameter that is a reference gets its own lifetime parameter.
- The second rule is if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters.
- The third rule is if there are multiple input lifetime parameters, but one of them is
&self
or&mut self
because this is a method, the lifetime ofself
is assigned to all output lifetime parameters.
1 | fn longest(x: &str, y: &str) -> &str { |
You can see that the second rule doesn’t apply because there is more than one input lifetime. The third rule doesn’t apply either, because longest is a function rather than a method, so none of the parameters are self. After working through all three rules, we still haven’t figured out what the return type’s lifetime is. This is why we got an error trying to compile the code above.
10-3-9 Lifetime Annotations in Method Definitions
Lifetime names for struct fields always need to be declared after the impl
keyword and then used after the struct’s name, because those lifetimes are part of the struct’s type.
In method signatures inside the impl
block, references might be tied to the lifetime of references in the struct’s fields, or they might be independent. In addition, the lifetime elision rules often make it so that lifetime annotations aren’t necessary in method signatures.
1 | impl<'a> ImportantExcerpt<'a> { |
The lifetime parameter declaration after impl
and use after the type name is required, but we’re not required to annotate the lifetime of the reference to self
because of the first elision rule.
1 | impl<'a> ImportantExcerpt<'a> { |
There are two input lifetimes, so Rust applies the first lifetime elision rule and gives both &self
and announcement
their own lifetimes. Then, because one of the parameters is &self
, the return type gets the lifetime of &self
, and all lifetimes have been accounted for.
10-3-10 The Static Lifetime
let s: &'static str = "I have a static lifetime.";
The text of this string is stored directly in the binary of your program, which is always available. Therefore, the lifetime of all string literals is 'static
.
10-4 Generic Type Parameters Trait Bounds and Lifetimes Together
1 | use std::fmt::Display; |
This is the longest
function now has an extra parameter named ann
of the generic type T
, which can be filled in by any type that implements the Display
trait as specified by the where
clause. This extra parameter will be printed before the function compares the lengths of the string slices, which is why the Display
trait bound is necessary. Because lifetimes are a type of generic, the declarations of the lifetime parameter 'a
and the generic type parameter T
go in the same list inside the angle brackets after the function name.
Generic type parameters let you apply the code to different types. Traits and trait bounds ensure that even though the types are generic, they’ll have the behavior the code needs.
11 Writing Automated Tests
11-1 How to Write Tests
- Set up needed data or state
- Run test code
- Assert results are what you expect
11-1-1 The Anatomy of a Test Function
At its simplest, a test in Rust is a function that’s annotated with the test
attribute. Attributes are metadata about pieces of Rust code. To change a function into a test function, add #[test]
on the line before fn
.
11-1-2 Checking Results with the assert!
Macro
1 | fn main() {} |
11-1-3 Testing Equality with the assert_eq!
and assert_ne!
Macros
The assert_ne!
macro will pass if the two values we give it are not equal and fail if they’re equal. This macro is most useful for cases when we’re not sure what a value will be, but we know what the value definitely won’t be if our code is functioning as we intend.
Under the surface, the assert_eq!
and assert_ne!
macros use the operators ==
and !=
, respectively. When the assertions fail, these macros print their arguments using debug formatting, which means the values being compared must implement the PartialEq
and Debug
traits.
- For structs and enums that you define, you’ll need to implement
PartialEq
to assert that values of those types are equal or not equal. - You’ll need to implement
Debug
to print the values when the assertion fails.
11-1-4 Adding Custom Failure Messages
1 | pub fn greeting(name: &str) -> String { |
11-1-5 Checking for Panics with should_panic
1 | pub struct Guess { |
11-1-6 Using Result<T, E>
in Tests
1 |
|
11-2 Controlling How Tests Are Run
11-2-1 Running Tests in Parallel or Consecutively
If you don’t want to run the tests in parallel or if you want more fine-grained control over the number of threads used:
1 | cargo test -- --test-threads=1 |
11-2-2 Showing Function Output
If we want to see printed values for passing tests as well:
1 | cargo test -- --nocapture |
11-2-3 Running a Subset of Tests by Name
- Running single tests:
cargo test func_name
- Filtering to run multiple tests:
cargo test some
will run functions whose name containssome
11-2-4 Ignoring Some Tests Unless Specifically Requested
1 |
|
If we want to run only the ignored tests:
1 | cargo test -- --ignored |
11-3 Test Organization
The Rust community thinks about tests in terms of two main categories: unit tests and integration tests.
Writing both kinds of tests is important to ensure that the pieces of your library are doing what you expect them to, separately and together.
11-3-1 Unit Tests
11-3-1-1 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
.
11-3-1-2 Testing Private Functions
Rust’s privacy rules do allow you to test private functions:
1 | pub fn add_two(a: i32) -> i32 { |
11-3-2 Integration Tests
11-3-2-1 The tests Directory
We create a tests directory at the top level of our project directory, next to src.
1 | use adder; |
We’ve added use adder
at the top because each test in the tests
directory is a separate crate, so we need to bring our library into each test crate’s scope. We don’t need to annotate any code in tests/integration_test.rs 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
.
1 | cargo test --test func_name |
11-3-2-2 Submodules in Integration Tests
1 | // tests/common.rs |
To avoid having common
appear in the test output, instead of creating tests/common.rs, we’ll create tests/common/mod.rs. This is an alternate naming convention that Rust also understands. Naming the file this way tells Rust not to treat the common
module as an integration test file.
Now the section in the test output will no longer appear. Files in subdirectories of the tests directory don’t get compiled as separate crates or have sections in the test output.
11-3-2-3 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. If the important functionality works, the small amount of code in the src/main.rs file will work as well, and that small amount of code doesn’t need to be tested.
18 Patterns and Matching
A pattern consists of some combination of the following:
- Literals
- Destructured arrays, enums, structs, or tuples
- Variables
- Wildcards
- Placeholders
18-1 All the Places Patterns Can Be Used
18-1-1 match
Arms
1 | match VALUE { |
One requirement for match
expressions is that they need to be exhaustive in the sense that all possibilities for the value in the match
expression must be accounted for.
18-1-2 Conditional if let
Expressions
1 | fn main() { |
The line if let Ok(age) = age
introduces a new shadowed age
variable that contains the value inside the Ok
variant. This means we need if age > 30
condition within that block: we can’t if let Ok(age) = age && age > 30
.
The downside of using if let
expressions is that the compiler doesn’t check exhaustiveness, whereas with match
expressions it does.
18-1-3 while let
Conditional Loops
1 | let mut stack = Vec::new(); |
18-1-4 for
Loops
1 | // for x in y the x is the pattern |
18-1-5 let
Statements
1 | let x = 5; |
x is a pattern that means “bind what matches here to the variable x. Because the name x
is the whole pattern, this pattern effectively means “bind everything to the variable x
, whatever the value is.”
If we wanted to ignore one or more of the values in the tuple, we could use _
or ..
.
18-1-6 Function Parameters
Function parameters can also be patterns.
1 | fn foo(x: i32) {} |
18-2 Refutability Whether a Pattern Might Fail to Match
Patterns that will match for any possible value passed are irrefutable. Patterns that can fail to match for some possible value are refutable.
Function parameters, let
statements, and for
loops can only accept irrefutable patterns, because the program cannot do anything meaningful when values don’t match.
The if let
and while let
expressions only accept refutable patterns, because by definition they’re intended to handle possible failure: the functionality of a conditional is in its ability to perform differently depending on success or failure.
1 | let Some(x) = some_option_value; |
If some_option_value
was a None
value, it would fail to match the pattern Some(x)
, meaning the pattern is refutable. Because we didn’t cover (and couldn’t cover!) every valid value with the pattern Some(x)
.
1 | if let Some(x) = some_option_value { |
If the pattern doesn’t match, the code will just skip the code in the curly brackets, giving it a way to continue validly.
It doesn’t make sense to use if let
with an irrefutable pattern:
1 | if let x = 5 { |
18-3 Pattern Syntax
18-3-1 Matching Literals
1 | let x = 1; |
This syntax is useful when you want your code to take an action if it gets a particular concrete value.
18-3-2 Matching Named Variables
1 | fn main() { |
18-3-3 Multiple Patterns
1 | let x = 1; |
18-3-4 Matching Ranges of Values with ..=
1 | let x = 5; |
Ranges are only allowed with numeric values or char
values, because the compiler checks that the range isn’t empty at compile time. The only types for which Rust can tell if a range is empty or not are char
and numeric values.
1 | let x = 'c'; |
18-3-5 Destructuring to Break Apart Values
18-3-5-1 Destructuring Structs
1 | struct Point { |
It’s common to want the variable names to match the field names to make it easier to remember which variables came from which fields.
There is a shorthand for patterns that match struct fields:
1 | struct Point { |
We can also destructure with literal values as part of the struct pattern rather than creating variables for all the fields.
1 | fn main() { |
18-3-5-2 Destructuring Enums
1 | enum Message { |
18-3-5-3 Destructuring Nested Structs and Enums
1 | enum Color { |
18-3-5-4 Destructuring Structs and Tuples
1 | let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 }); |
18-3-6 Ignoring Values in a Pattern
18-3-6-1 Ignoring an Entire Value with _
1 | fn foo(_: i32, y: i32) { |
18-3-6-2 Ignoring Parts of a Value with a Nested _
1 | let mut setting_value = Some(5); |
In the first match arm, we don’t need to match on or use the values inside either Some
variant, but we do need to test for the case when setting_value
and new_setting_value
are the Some
variant.
1 | let numbers = (2, 4, 8, 16, 32); |
18-3-6-3 Ignoring an Unused Variable by Starting Its Name with _
1 | fn main() { |
The syntax _x
still binds the value to the variable, whereas _
doesn’t bind at all.
1 | let s = Some(String::from("Hello!")); |
We’ll receive an error because the s
value will still be moved into _s
, which prevents us from using s
again. However, using the underscore by itself doesn’t ever bind to the value.
1 | let s = Some(String::from("Hello!")); |
18-3-6-4 Ignoring Remaining Parts of a Value with ..
1 | struct Point { |
The syntax ..
will expand to as many values as it needs to be.
1 | fn main() { |
If it is unclear which values are intended for matching and which should be ignored, Rust will give us an error.
1 | fn main() { |
18-3-7 Extra Conditionals with Match Guards
A match guard is an additional if
condition specified after the pattern in a match
arm that must also match, along with the pattern matching, for that arm to be chosen. Match guards are useful for expressing more complex ideas than a pattern alone allows.
1 | let num = Some(4); |
There is no way to express the if x < 5
condition within a pattern, so the match guard gives us the ability to express this logic.
1 | fn main() { |
Use the or operator |
in a match guard to specify multiple patterns.
1 | let x = 4; |
Only matches if the value of x
is equal to 4
, 5
, or 6
and if y
is true
.
18-3-8 @
Bindings
The at operator (@
) lets us create a variable that holds a value at the same time we’re testing that value to see whether it matches a pattern.
1 | enum Message { |
In the second arm, where we only have a range specified in the pattern, the code associated with the arm doesn’t have a variable that contains the actual value of the id
field. The id
field’s value could have been 10, 11, or 12, but the code that goes with that pattern doesn’t know which it is. The pattern code isn’t able to use the value from the id
field, because we haven’t saved the id
value in a variable.