Introduction
After reading The Book, I tried to take as many notes as possible to condense into a high level overview. My idea is that before you read The Book, you could skim through this website super quick and pickup the high level concepts before diving into anything deeper. Ideally, after reading through this, you will not get stuck in a lot of the parts most people do when learning Rust.
Best of luck and happy reading!
Rusty Cloud
If you are reading this then you are already aware what the Rust programming language is and maybe have even heard a couple things about it. Some of those things are "It's fast", "It is the hot new language", "It is a memory safe language". You probably also heard that "It has a steep learning curve". While this may be true, I think a couple things work in the favor of Rust to reduce that curve quite heavily.
Rust focuses on 3 main pillars:
- Performance
- Rust is blazingly fast and efficient
- Reliability
- Code can finally be "done". Many "crates" haven't been touched in years, but still get used and pulled on a daily basis
- Productivity
- Rust has tons of documentation with great compile time error messages that prevent common in production mistakes
One thing is, if you are coming from a language without a Garbage Collector, and have had to manually allocate and clean memory, then you shouldn't have too difficult of a time picking up the quirks of Rust. If you are like me, you are coming from Python, Java/Scala, Go, or some other garbage collected language where we have never had to think about memory allocation. Essentially, as variables, functions, data structures build up in a program, garbage collected languages will monitor whether it needs to discard no longer used memory. Here is a quick example comparing python
to Rust
.
## Assign `test` to `x`
x = "test"
## Assign `x`'s value to `y`
y = x
## Print the value of x (should get "test")
print(x)
## Change the value of `x` to `another`
x = "another"
## Print the value of `x` (should be `another`)
print(x)
## Print the value of `y` (should be `test`)
print(y)
output:
"test"
"another"
"test"
As we can see y
equals the value of x
at the time x
's value was assigned to y
. Let's try to run something similar in Rust...
Hit the play button in the code box
fn main() { // initiate `x` with a String value `test` let x = String::from("test"); // initiate `y` with the value of `x` let y = x; // print the values println!("{x}"); println!("{y}"); }
We get an error talking about moving values and borrowing. Don't worry about what they mean right now, just know that Rust can seem strange at first. We will dive into this much more and is part of the foundation of the Rust language.
Another thing working in reducing the Learning curve's favor is the amazing amount of resources and community that is availabe to you. I personally recommend reading "The Book" first, but here is a list of good resources:
Fundamentals
This will cover all of those general knowledge programming concepts you need before you start writing in any language. Some of these will be very familiar because the concepts are present in other languages (e.g. if-else statements, adding types to variables, creating functions), but some will be uniqure to Rust (e.g. Borrowing and Shadowing).
Feel free to skim this section if you are coming from other compiled languages, but I highly recommend not skipping it as there may be some Rust quirks buried in some of the simpler concepts.
Variables
Variables and Mutability
By default, variables are immutable in Rust. This means that once a value is bound to a variable name, you are no longer able to make changes to it. For example, the following will give you an error:
fn main() { // Let's assign the value of 5 to `x` let x = 5; println!("The value of x is: {x}"); // Let's try reassigning `x` to a value of 6 x = 6; println!("The value of x is: {x}"); }
These copiler errors can seem burdensome and frustrating at first, but you will soon grow to love the rich and detailed feedback of the Rust compilier. Rust's compiler even tells you how to correct the error in most instances. By reading the output of the compiler, we can see that we need to add a mut
identifier to our variable of x
. mut
will turn our variable into a mutable instance. That means we can make changes to it, but this can also be dangerous. Which is why it is has to be specified, so you are aware that you are doing something dangerous (this is a recurring theme in Rust). Let now change our x
variable to be mutable :
fn main() { // Let's assign the value of 5 to `x` let mut x = 5; println!("The value of x is: {x}"); // Let's try reassigning `x` to a value of 6 x = 6; println!("The value of x is: {x}"); }
Constants
Like immutable variables, constants are values that are bound to a name and are not allowed to change, but there are a few differences between constants and variables. You are not allowed to use mut
with constants. You also use const
keyword when declaring a constant and you need to annotate the type (i.e. if my constanct is an integer I need to put : u8
after the name of the constant). Constants can be delared in any scope, they also need to be set and cannot be the result of a runtime expression. Rust's standard naming convention for constants is all uppercase with an underscore between words.
fn main() { let FOUR_HOURS_IN_SECONDS = 4 * 60 * 60; println!("{FOUR_HOURS_IN_SECONDS}"); }
Shadowing
You can declare a new variable with the same name as a previous variable, you would then say the first variable is shadowed by the second. the second variable overshadows the first, taking any uses of the variable name to itself until either it itself is shadowed or the scope ends. Here is an example:
fn main() { // Assign 5 to `x` let x = 5; // Reassign the variable of `x` let x = x + 1; { // Assign `x` a new value to this inner scope let x = x * 2; println!("The value of x in the inner scope is: {x}"); } println!("The value of x in the outer scope is: {x}"); }
You might be curious why this ran but the previous code gave an error when we tried to reassign x
. Look close at the usage of let
in both code blocks. Shadowing is different from marking a variable as mut because we’ll get a compile-time error if we accidentally try to reassign to this variable without using the let keyword. 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.
Let's test this out in practice. We are going to initialize spaces
as a string type then reassign it as a integer type:
fn main() { let spaces = " "; let spaces = spaces.len(); }
We don't get an error because we reinitialzied with let
. Now, let's try it without let
and read the compiler error:
fn main() { let mut spaces = " "; spaces = spaces.len(); }
Even though spaces is mutable, we tried to assign it a different type, which throws a compiler errror.
Exercises
Exercise 1
fn main() { x = 5; println!("x has the value {}", x); }
Solution
fn main() { let x = 5; // You needed to add a `let` in front of the variable `x` println!("x has the value {}", x); }
Exercise 2
fn main() { let x; if x == 10 { println!("x is ten!"); } else { println!("x is not ten!"); } }
Solution
fn main() { let x: u8 = 10; // Make sure you intiailize `x` if x == 10 { println!("x is ten!"); } else { println!("x is not ten!"); } }
Exercise 3
fn main() { let x: i32; println!("Number {}", x); }
Solution
fn main() { let x: i32 = 0; // Make sure you initialize `x` println!("Number {}", x); }
Exercise 4
fn main() { let x = 3; println!("Number {}", x); x = 5; // don't change this line println!("Number {}", x); }
Solution
fn main() { let mut x = 3; // add the `mut` to make the variable mutable to change it to 5 println!("Number {}", x); x = 5; // don't change this line println!("Number {}", x); }
Exercise 5
fn main() { let number = "T-H-R-E-E"; // don't change this line println!("Spell a Number : {}", number); number = 3; // don't rename this variable println!("Number plus two is : {}", number + 2); }
Solution
fn main() { let number = "T-H-R-E-E"; // don't change this line println!("Spell a Number : {}", number); // Change `number` to a different scope { let number: u8 = 3; // don't rename this variable println!("Number plus two is : {}", number + 2); } }
Exercise 6
const NUMBER = 3; fn main() { println!("Number {}", NUMBER); }
Solution
const NUMBER: u8 = 3; fn main() { println!("Number {}", NUMBER); }
Data Types
Data Types tell Rust which can of data is being specified so it knows how to work with that data. Rust has two major subsets of data types: scalar and compound.
Rust is a statically typed language, which means it needs to know the types of all variables at compile time.
Scalar Types
A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.
Integer
An integer is a number without a fractional component. E.g. 1, 2, 3, 4, 5... You can use the prefix u
for unsigned integer types (i.e. non-negative numbers don't need a sign -
). Or you can use i
for integers that could potentially be negative.
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 |
Signed and unsigned refer to whether it’s possible for the number to be negative—in other words, whether the number needs to have a sign with it (signed) or whether it will only ever be positive and can therefore be represented without a sign (unsigned). Each signed variant can store numbers from –(2n – 1) to 2n – 1 – 1 inclusive, where n is the number of bits that variant uses. So an i8 can store numbers from –(27) to 27 – 1, which equals –128 to 127. Unsigned variants can store numbers from 0 to 2n – 1, so a u8 can store numbers from 0 to 28 – 1, which equals 0 to 255.
Number literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
So how do you know which type of integer to use? If you’re unsure, Rust’s defaults are generally good places to start: integer types default to i32.
Floating-Point Types
Rust also has two primitive types for floating-point numbers, which are numbers with decimal points. Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively. The default type is f64 because on modern CPUs, it’s roughly the same speed as f32 but is capable of more precision. All floating-point types are signed.
fn main() { let x: f32 = 4.234; }
The Boolean Type
As in most other programming languages, a Boolean type in Rust has two possible values: true and false. Booleans are one byte in size.
fn main() { let t = true; let f: bool = false; }
The Character Type
Rust’s char type is the language’s most primitive alphabetic type.
fn main() { let a = 'b'; let z: char = 'Z'; }
Note that we specify char literals with single quotes, as opposed to string literals, which use double quotes.
Compund Types
Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.
The Tuple Type
A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size. We create a tuple by writing a comma-separated list of values inside parentheses.
fn main() { let tup: (i32, f64, u8) = (1000, 3.14159, 7); let (a, b, c) = tup; println!("The value of b is: {b}"); }
It then uses a pattern with let to take tup and turn it into three separate variables, a
, b
, and c
. This is called destructuring because it breaks the single tuple into three parts.
We can also access a tuple element directly by using a period (.) followed by the index of the value we want to access.
fn main() { let tup: (i32, f64, u8) = (1000, 3.14159, 7); let one_thousand = tup.0; let text_pi = tup.1; let lucky_number_seven = tup.2; println!("{text_pi}"); }
The tuple without any values has a special name, unit.
The Array Type
Another way to have a collection of multiple values is with an array. Unlike a tuple, every element of an array must have the same type. Unlike arrays in some other languages, arrays in Rust have a fixed length.
fn main() { let a = [1, 2, 3, 4, 5]; println!("{:?}", a); }
Arrays are useful when you want your data allocated on the stack rather than the heap or when you want to ensure you always have a fixed number of elements. An array isn’t as flexible as the vector type, though. A vector is a similar collection type provided by the standard library that is allowed to grow or shrink in size. If you’re unsure whether to use an array or a vector, chances are you should use a vector. However, arrays are more useful when you know the number of elements will not need to change.
You write an array’s type using square brackets with the type of each element, a semicolon, and then the number of elements in the array.
fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; println!("{:?}", a) }
You can also initialize an array to contain the same value for each element by specifying the initial value, followed by a semicolon, and then the length of the array in square brackets.
fn main() { let a = [5; 10]; println!("{:?}", a); }
An array is a single chunk of memory of a known, fixed size that can be allocated on the stack. You can access elements of an array using indexing,
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; println!("{first} then {second}"); }
Let’s see what happens if you try to access an element of an array that is past the end of the array.
fn main() { let a = [1, 2, 3, 4, 5]; let err = a[8]; }
The program resulted in a runtime error at the point of using an invalid value in the indexing operation.
Exercises
Exercise 1
fn main() { // Booleans (`bool`) let is_morning = true; if is_morning { println!("Good morning!"); } let // Finish the rest of this line like the example! Or make it be false! if is_evening { println!("Good evening!"); } }
Solution
fn main() { // Booleans (`bool`) let is_morning = true; if is_morning { println!("Good morning!"); } // let is_evening = true; // Finish the rest of this line like the example! Or make it be false! if is_evening { println!("Good evening!"); } }
Exercise 2
fn main() { // Characters (`char`) // Note the _single_ quotes, these are different from the double quotes // you've been seeing around. let my_first_initial = 'C'; if my_first_initial.is_alphabetic() { println!("Alphabetical!"); } else if my_first_initial.is_numeric() { println!("Numerical!"); } else { println!("Neither alphabetic nor numeric!"); } let // Finish this line like the example! What's your favorite character? // Try a letter, try a number, try a special character, try a character // from a different language than your own, try an emoji! if your_character.is_alphabetic() { println!("Alphabetical!"); } else if your_character.is_numeric() { println!("Numerical!"); } else { println!("Neither alphabetic nor numeric!"); } }
Solution
fn main() { // Characters (`char`) // Note the _single_ quotes, these are different from the double quotes // you've been seeing around. let my_first_initial = 'C'; if my_first_initial.is_alphabetic() { println!("Alphabetical!"); } else if my_first_initial.is_numeric() { println!("Numerical!"); } else { println!("Neither alphabetic nor numeric!"); } // let your_character = 'A'; // Finish this line like the example! What's your favorite character? // Try a letter, try a number, try a special character, try a character // from a different language than your own, try an emoji! if your_character.is_alphabetic() { println!("Alphabetical!"); } else if your_character.is_numeric() { println!("Numerical!"); } else { println!("Neither alphabetic nor numeric!"); } }
Exercise 3
fn main() { let a = ??? if a.len() >= 100 { println!("Wow, that's a big array!"); } else { println!("Meh, I eat arrays like that for breakfast."); panic!("Array not big enough, more elements needed") } }
Solution
fn main() { let a = [0; 100]; // if a.len() >= 100 { println!("Wow, that's a big array!"); } else { println!("Meh, I eat arrays like that for breakfast."); } }
Exercise 4
#![allow(unused)] fn main() { #[test] fn slice_out_of_array() { let a = [1, 2, 3, 4, 5]; let nice_slice = ??? assert_eq!([2, 3, 4], nice_slice) } }
Solution
#![allow(unused)] fn main() { #[test] fn slice_out_of_array() { let a = [1, 2, 3, 4, 5]; // let nice_slice = &a[1..4]; // assert_eq!([2, 3, 4], nice_slice) } }
Exercise 5
fn main() { let cat = ("Furry McFurson", 3.5); let /* your pattern here */ = cat; println!("{} is {} years old.", name, age); }
Solution
fn main() { let cat = ("Furry McFurson", 3.5); let (name, age)/* your pattern here */ = cat; // println!("{} is {} years old.", name, age); }
Exercise 6
#![allow(unused)] fn main() { #[test] fn indexing_tuple() { let numbers = (1, 2, 3); // Replace below ??? with the tuple indexing syntax. let second = ???; assert_eq!(2, second, "This is not the 2nd number in the tuple!") } }
Solution
#![allow(unused)] fn main() { #[test] fn indexing_tuple() { let numbers = (1, 2, 3); // Replace below ??? with the tuple indexing syntax. let second = numbers.1; // assert_eq!(2, second, "This is not the 2nd number in the tuple!") } }
Functions
You are already familiar with functions, becuase in previous examples we have seen the use of the fn main() {}
function. The main
function is the entrypoint to many programs. You use fn
to declare new functions. Rust code uses snake case as the conventional style for function and variable names, in which all letters are lowercase and underscores separate words.
fn main() { println!("Hello, World"); another_function(); } fn another_function() { println!("another function"); }
Parameters
We can define functions to have parameters, which are special variables that are part of a function’s signature. When a function has parameters, you can provide it with concrete values for those parameters. Technically, the concrete values are called arguments, but in casual conversation, people tend to use the words parameter and argument interchangeably for either the variables in a function’s definition or the concrete values passed in when you call a function.
fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {x}"); }
In function signatures, you must declare the type of each parameter. This is a deliberate decision in Rust’s design: requiring type annotations in function definitions means the compiler almost never needs you to use them elsewhere in the code to figure out what type you mean.
fn main() { print_labeled_measurement(5, 'h'); } fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); }
Statments and Expressions
Function bodies are made up of a series of statements optionally ending in an expression. So far, the functions we’ve covered haven’t included an ending expression, but you have seen an expression as part of a statement. Because Rust is an expression-based language, this is an important distinction to understand. Other languages don’t have the same distinctions, so let’s look at what statements and expressions are and how their differences affect the bodies of functions.
- Statements are instructions that perform some action and do not return a value.
- Expressions evaluate to a resultant value.
Function definitions are also statements and staements don't return values. Expressions evaluate to a value and make up most of the rest of the code that you’ll write in Rust.
- Calling a function is an expression.
- Calling a macro is an expression.
Expressions do not include ending semicolons. If you add a semicolon to the end of an expression, you turn it into a statement, and it will then not return a value. Keep this in mind as you explore function return values and expressions next.
Functions with Return Values
Functions can return values to the code that calls them. We don’t name return values, but we must 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.
fn five() -> i32 { 5 } fn main() { let x = five(); println!("The value of x is: {x}", x) }
There are no function calls, macros, or even let statements in the five function—just the number 5 by itself. That’s a perfectly valid function in Rust.
The five function has no parameters and defines the type of the return value, but the body of the function is a lonely 5 with no semicolon because it’s an expression whose value we want to return.
Exercises
Control Flow
The ability to run some code depending on whether a condition is true and to run some code repeatedly while a condition is true are basic building blocks in most programming languages. The most common constructs that let you control the flow of execution of Rust code are if expressions and loops.
if
Expressions
An if expression allows you to branch your code depending on conditions. All if expressions start with the keyword if, followed by a condition.
fn main() { let x = 1; if x > 5 { println!("x is greater than five and has a value of: {x}"); } else { println!("x is less than five and has a value of: {x}"); } }
Unlike languages such as Ruby and JavaScript, Rust will not automatically try to convert non-Boolean types to a Boolean.
Handling Multiple Conditions with else if
You can use multiple conditions by combining if and else in an else if expression.
fn main() { let x = 10; if x % 4 == 0 { println!("{x} is divisible by 4"); } else if x % 3 == 0{ println!("{x} is divisible by 3"); } else { println!("{x} is not divisible by 4 or 3"); } }
Using too many else if expressions can clutter your code, so if you have more than one, you might want to refactor your code.
Using an if let
Because if is an expression, we can use it on the right side of a let statement to assign the outcome to a variable.
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of the number is: {number}"); }
Remember that blocks of code evaluate to the last expression in them, and numbers by themselves are also expressions. In this case, the value of the whole if expression depends on which block of code executes. This means the values that have the potential to be results from each arm of the if must be the same type.
Repition with Loops
It’s often useful to execute a block of code more than once. For this task, Rust provides several loops, which will run through the code inside the loop body to the end and then start immediately back at the beginning.
The following code will just give a
playground timeout
error, if you run this on your own machine, the loop will continue forever. Thanks for not breaking my server!
fn main() { loop { println!("Too infinity, and beyond!"); } }
Rust also provides a way to break out of a loop using code. You can place the break keyword within the loop to tell the program when to stop executing the loop. We can use continue, which in a loop tells the program to skip over any remaining code in this iteration of the loop and go to the next iteration.
fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}"); }
Before the loop, we declare a variable named counter and initialize it to 0. Then we declare a variable named result to hold the value returned from the loop. On every iteration of the loop, we add 1 to the counter variable, and then check whether the counter is equal to 10. When it is, we use the break keyword with the value counter * 2. After the loop, we use a semicolon to end the statement that assigns the value to result.
Loop labes to Disambiguate Between Multiple Loops
If you have loops within loops, break and continue apply to the innermost loop at that point. You can optionally specify a loop label on a loop that you can then use with break or continue to specify that those keywords apply to the labeled loop instead of the innermost loop. Loop labels must begin with a single quote.
fn main() { let mut count = 0; 'counting_up: loop { println!("count = {count}"); let mut remaining = 10; loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("End count = {count}"); }
Conditional Loops with while
A program will often need to evaluate a condition within a loop. While the condition is true, the loop runs. When the condition ceases to be true, the program calls break, stopping the loop. It’s possible to implement behavior like this using a combination of loop, if, else, and break;
fn main() { let mut x = 3; while x != 0 { println!("{x}"); x -= 1; } println!("LIFTOFF!"); }
This construct eliminates a lot of nesting that would be necessary if you used loop, if, else, and break, and it’s clearer. While a condition evaluates to true, the code runs; otherwise, it exits the loop.
Looping Through a collection with for
You can choose to use the while construct to loop over the elements of a collection, such as 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; } }
However, this approach is error prone; we could cause the program to panic if the index value or test condition is incorrect. For example, if you changed the definition of the a array to have four elements but forgot to update the condition to while index < 4.
As a more concise alternative, you can use a for loop and execute some code for each item in a collection.
fn main() { let a = [10, 20, 30, 40, 50]; for i in a { println!("The value is: {i}"); } }
The safety and conciseness of for loops make them the most commonly used loop construct in Rust.
Exercises
Ownership
Ownership is Rust’s most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector, so it’s important to understand how ownership works.
Before you get started please try to understand the following concepts of what the STACK and HEAP are in reference to computer memory!
The Stack and the Heap
Many programming languages don’t require you to think about the stack and the heap very often. But in a systems programming language like Rust, whether a value is on the stack or the heap affects how the language behaves and why you have to make certain decisions. Parts of ownership will be described in relation to the stack and the heap later in this chapter, so here is a brief explanation in preparation.
Both the stack and the heap are parts of memory available to your code to use at runtime, but they are structured in different ways. The stack stores values in the order it gets them and removes the values in the opposite order. This is referred to as last in, first out. Think of a stack of plates: when you add more plates, you put them on top of the pile, and when you need a plate, you take one off the top. Adding or removing plates from the middle or bottom wouldn’t work as well! Adding data is called pushing onto the stack, and removing data is called popping off the stack. All data stored on the stack must have a known, fixed size. Data with an unknown size at compile time or a size that might change must be stored on the heap instead.
The heap is less organized: when you put data on the heap, you request a certain amount of space. The memory allocator finds an empty spot in the heap that is big enough, marks it as being in use, and returns a pointer, which is the address of that location. This process is called allocating on the heap and is sometimes abbreviated as just allocating (pushing values onto the stack is not considered allocating). Because the pointer to the heap is a known, fixed size, you can store the pointer on the stack, but when you want the actual data, you must follow the pointer. Think of being seated at a restaurant. When you enter, you state the number of people in your group, and the host finds an empty table that fits everyone and leads you there. If someone in your group comes late, they can ask where you’ve been seated to find you.
Pushing to the stack is faster than allocating on the heap because the allocator never has to search for a place to store new data; that location is always at the top of the stack. Comparatively, allocating space on the heap requires more work because the allocator must first find a big enough space to hold the data and then perform bookkeeping to prepare for the next allocation.
Accessing data in the heap is slower than accessing data on the stack because you have to follow a pointer to get there. Contemporary processors are faster if they jump around less in memory. Continuing the analogy, consider a server at a restaurant taking orders from many tables. It’s most efficient to get all the orders at one table before moving on to the next table. Taking an order from table A, then an order from table B, then one from A again, and then one from B again would be a much slower process. By the same token, a processor can do its job better if it works on data that’s close to other data (as it is on the stack) rather than farther away (as it can be on the heap).
When your code calls a function, the values passed into the function (including, potentially, pointers to data on the heap) and the function’s local variables get pushed onto the stack. When the function is over, those values get popped off the stack.
Keeping track of what parts of code are using what data on the heap, minimizing the amount of duplicate data on the heap, and cleaning up unused data on the heap so you don’t run out of space are all problems that ownership addresses. Once you understand ownership, you won’t need to think about the stack and the heap very often, but knowing that the main purpose of ownership is to manage heap data can help explain why it works the way it does.
Ownership Explained
- Ownership Rules
- Variable Scope
- The String Type
- Memory and Allocation
- Ownership and Functions
- Return Values and Scope
Ownership is a set of rules that govern how a Rust program manages memory. All programs have to manage the way they use a computer’s memory while running. Some languages have garbage collection that regularly looks for no-longer-used memory as the program runs; in other languages, the programmer must explicitly allocate and free the memory. Rust uses a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks. If any of the rules are violated, the program won’t compile. None of the features of ownership will slow down your program while it’s running.
Ownership Rules
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
#![allow(unused)] fn main() { let s = "hello"; }
Variable Scope
- When s comes into scope, it is valid.
- It remains valid until it goes out of scope.
The String Type
The types covered previously are of a known size, can be stored on the stack and popped off the stack when their scope is over, and can be quickly and trivially copied to make a new, independent instance if another part of code needs to use the same value in a different scope. But we want to look at data that is stored on the heap and explore how Rust knows when to clean up that data, and the String type is a great example.
We’ve already seen string literals (like the variable s
above), where a string value is hardcoded into our program. String literals are convenient, but they aren’t suitable for every situation in which we may want to use text. One reason is that they’re immutable.
Rust has a second string type, String. This type manages data allocated on the heap and as such is able to store an amount of text that is unknown to us at compile time. You can create a String from a string literal using the from function:
#![allow(unused)] fn main() { let s = String::from("hello"); }
The double colon :: operator allows us to namespace this particular from function under the String type rather than using some sort of name like string_from.
#![allow(unused)] fn main() { let mut s = String::from("hello"); s.push_str(", world"); // push_str() appends a lieteral to a string println!("{s}"); }
Memory and Allocation
In the case of a string literal, we know the contents at compile time, so the text is hardcoded directly into the final executable. This is why string literals are fast and efficient. But these properties only come from the string literal’s immutability.
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 memory allocator at runtime.
- We need a way of returning this memory to the allocator when we’re done with our String.
Rust takes a different path: the memory is automatically returned once the variable that owns it goes out of scope.
#![allow(unused)] fn main() { { let s = String::from("hello"); // s is valid from this point forward // do whatever with s } // this scope is now over and s is no longer valid }
There is a natural point at which we can return the memory our String needs to the allocator: when s
goes out of scope. When a variable goes out of scope, Rust calls a special function for us. This function is called drop
, and it’s where the author of String can put the code to return the memory. Rust calls drop automatically at the closing curly bracket.
Variables and Data Interacting with Move
A String is made up of three parts, shown on the left: a pointer to the memory that holds the contents of the string, a length, and a capacity. This group of data is stored on the stack. On the right is the memory on the heap that holds the contents.
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; }
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.
The representation does not look like the picture below, which is what memory would look like if Rust instead copied the heap data as well. If Rust did this, the operation s2 = s1
could be very expensive in terms of runtime performance if the data on the heap were large.
Earlier, we said that when a variable goes out of scope, Rust automatically calls the drop function and cleans up the heap memory for that variable. But two images above shows 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 we mentioned previously. Freeing memory twice can lead to memory corruption, which can potentially lead to security vulnerabilities. To ensure memory safety, after the line let s2 = s1;
, Rust considers s1
as no longer valid.
You should receive an error on the following code
#![allow(unused)] fn main() { let s1 = String::from("Hello"); let s2 = s1; println!("{s1}, world"); }
If you’ve heard the terms shallow copy and deep copy while working with other languages, the concept of copying the pointer, length, and capacity without copying the data probably sounds like making a shallow copy. But because Rust also invalidates the first variable, instead of being called a shallow copy, it’s known as a move. In this example, we would say that s1
was moved into s2
.
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.
Variables and Data Interacting with 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
.
#![allow(unused)] fn main() { let s1 = String::from("Hello"); let s2 = s1.clone(); println!("s1 = {s1}, s2 = {s2}"); }
When you see a call to `clone``, you know that some arbitrary code is being executed and that code may be expensive. It’s a visual indicator that something different is going on.
Stack Only Data: Copy
#![allow(unused)] fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); }
But this code seems to contradict what we just learned: 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. That means there’s no reason we would want to prevent x
from being valid after we create the variable y
. In other words, 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.
So, what types implement the Copy
trait? You can check the documentation for the given type to be sure, but as a general rule, any group of simple scalar values can implement Copy
, and nothing that requires allocation or is some form of resource can implement Copy
. Here are some of the types that implement Copy:
- All the integer types, such as u32.
- The Boolean type, bool, with values true and false.
- All the floating-point types, such as f64.
- The character type, char.
- Tuples, if they only contain types that also implement Copy. For example, (i32, i32) implements Copy, but (i32, String) does not.
Ownership and Functions
fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // x would move into the function, // but i32 is Copy, so it's okay to still // use x afterward } // Here, x goes out of scope, then s. However, because s's value was moved, // nothing special happens. fn takes_ownership(some_string: String) { // some_string comes into scope println!("{some_string}"); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{some_integer}"); } // Here, some_integer goes out of scope. Nothing special happens.
If we tried to use s after the call to takes_ownership, Rust would throw a compile-time error.
Return Values and Scope
Returning values can also transfer ownership.
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String::from("yours"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns a String. fn takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function }
While this works, taking ownership and then returning ownership with every function is a bit tedious.
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{s2}' is {len}."); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String (s, length) }
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 using a value without transferring ownership, called references.
References and Borrowing
Exercises
The Slice Type
Structuring Data
Structs
Enums
Pattern Matching
Error Handling
Generics, Traits, and Lifetimes
Iterators and Closures
Closures
Iterators
Pointers
Using Box
Dereferencing
Drop Trait
Rc
RefCell
Reference Cycles
Concurrency
Using Threads
Message Passing
Shared State Concurrency
Send and Sync
Object Oriented Features
Characteristics of Object Oriented Programming
Using Trait Objects
Implementing OO Design Pattern
Patterns and Matching
Where Patterns can be used
Refutability
Pattern Syntax
Advanced Features
Unsafe Rust
Advanced Traits
Advanced Types
Advanced Functions and Closures
Macros
Reference
Appendix A: Keywords
The following list contains keywords that are reserved for current or future use by the Rust language. As such, they cannot be used as identifiers (except as raw identifiers as we’ll discuss in the “Raw Identifiers” section). Identifiers are names of functions, variables, parameters, struct fields, modules, crates, constants, macros, static values, attributes, types, traits, or lifetimes.
Keywords Currently in Use
The following is a list of keywords currently in use, with their functionality described.
as
- perform primitive casting, disambiguate the specific trait containing an item, or rename items inuse
statementsasync
- return aFuture
instead of blocking the current threadawait
- suspend execution until the result of aFuture
is readybreak
- exit a loop immediatelyconst
- define constant items or constant raw pointerscontinue
- continue to the next loop iterationcrate
- in a module path, refers to the crate rootdyn
- dynamic dispatch to a trait objectelse
- fallback forif
andif let
control flow constructsenum
- define an enumerationextern
- link an external function or variablefalse
- Boolean false literalfn
- define a function or the function pointer typefor
- loop over items from an iterator, implement a trait, or specify a higher-ranked lifetimeif
- branch based on the result of a conditional expressionimpl
- implement inherent or trait functionalityin
- part offor
loop syntaxlet
- bind a variableloop
- loop unconditionallymatch
- match a value to patternsmod
- define a modulemove
- make a closure take ownership of all its capturesmut
- denote mutability in references, raw pointers, or pattern bindingspub
- denote public visibility in struct fields,impl
blocks, or modulesref
- bind by referencereturn
- return from functionSelf
- a type alias for the type we are defining or implementingself
- method subject or current modulestatic
- global variable or lifetime lasting the entire program executionstruct
- define a structuresuper
- parent module of the current moduletrait
- define a traittrue
- Boolean true literaltype
- define a type alias or associated typeunion
- define a union; is only a keyword when used in a union declarationunsafe
- denote unsafe code, functions, traits, or implementationsuse
- bring symbols into scopewhere
- denote clauses that constrain a typewhile
- loop conditionally based on the result of an expression
Keywords Reserved for Future Use
The following keywords do not yet have any functionality but are reserved by Rust for potential future use.
abstract
become
box
do
final
macro
override
priv
try
typeof
unsized
virtual
yield
Raw Identifiers
Raw identifiers are the syntax that lets you use keywords where they wouldn’t
normally be allowed. You use a raw identifier by prefixing a keyword with r#
.
For example, match
is a keyword. If you try to compile the following function
that uses match
as its name:
Filename: src/main.rs
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
you’ll get this error:
error: expected identifier, found keyword `match`
--> src/main.rs:4:4
|
4 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword
The error shows that you can’t use the keyword match
as the function
identifier. To use match
as a function name, you need to use the raw
identifier syntax, like this:
Filename: src/main.rs
fn r#match(needle: &str, haystack: &str) -> bool { haystack.contains(needle) } fn main() { assert!(r#match("foo", "foobar")); }
This code will compile without any errors. Note the r#
prefix on the function
name in its definition as well as where the function is called in main
.
Raw identifiers allow you to use any word you choose as an identifier, even if
that word happens to be a reserved keyword. This gives us more freedom to
choose identifier names, as well as lets us integrate with programs written in
a language where these words aren’t keywords. In addition, raw identifiers
allow you to use libraries written in a different Rust edition than your crate
uses. For example, try
isn’t a keyword in the 2015 edition but is in the 2018
edition. If you depend on a library that’s written using the 2015 edition and
has a try
function, you’ll need to use the raw identifier syntax, r#try
in
this case, to call that function from your 2018 edition code. See Appendix
E for more information on editions.
Appendix B: Operators and Symbols
This appendix contains a glossary of Rust’s syntax, including operators and other symbols that appear by themselves or in the context of paths, generics, trait bounds, macros, attributes, comments, tuples, and brackets.
Operators
Table B-1 contains the operators in Rust, an example of how the operator would appear in context, a short explanation, and whether that operator is overloadable. If an operator is overloadable, the relevant trait to use to overload that operator is listed.
Table B-1: Operators
Operator | Example | Explanation | Overloadable? |
---|---|---|---|
! | `ident!(...)`, `ident!{...}`, `ident![...]` | Macro expansion | |
! | !expr | Bitwise or logical complement | `Not` |
!= | `expr != expr` | Nonequality comparison | `PartialEq` |
% | `expr % expr` | Arithmetic remainder | `Rem` |
%= | `var %= expr` | Arithmetic remainder and assignment | `RemAssign` |
& | &expr , &mut expr | Borrow | |
& | &type , &mut type , &'a type , &'a mut type | Borrowed pointer type | |
& | `expr & expr` | Bitwise AND | `BitAnd` |
&= | `var &= expr` | Bitwise AND and assignment | `BitAndAssign` |
&& | `expr && expr` | Short-circuiting logical AND | |
* | `expr * expr` | Arithmetic multiplication | `Mul` |
*= | `var *= expr` | Arithmetic multiplication and assignment | `MulAssign` |
* | *expr | Dereference | `Deref` |
* | \*const type , \*mut type | Raw pointer | |
+ | `trait + trait`, 'a + trait | Compound type constraint | |
+ | `expr + expr` | Arithmetic addition | `Add` |
+= | `var += expr` | Arithmetic addition and assignment | `AddAssign` |
, | `expr, expr` | Argument and element separator | |
\- | \- expr | Arithmetic negation | `Neg` |
- | `expr - expr` | Arithmetic subtraction | `Sub` |
-= | `var -= expr` | Arithmetic subtraction and assignment | `SubAssign` |
-> | `fn(...) -> type`, <code>|...| -> type</code> | Function and closure return type | |
. | `expr.ident` | Member access | |
.. | .. , `expr..`, ..expr , `expr..expr` | Right-exclusive range literal | `PartialOrd` |
..= | ..=expr , `expr..=expr` | Right-inclusive range literal | `PartialOrd` |
.. | ..expr | Struct literal update syntax | |
.. | `variant(x, ..)`, `struct_type { x, .. }` | “And the rest” pattern binding | |
... | `expr...expr` | (Deprecated, use ..= instead) In a pattern: inclusive range pattern | |
/ | `expr / expr` | Arithmetic division | `Div` |
/= | `var /= expr` | Arithmetic division and assignment | `DivAssign` |
: | `pat: type`, `ident: type` | Constraints | |
: | `ident: expr` | Struct field initializer | |
: | 'a: loop {...} | Loop label | |
; | `expr;` | Statement and item terminator | |
; | \[...; len\] | Part of fixed-size array syntax | |
<< | `expr << expr` | Left-shift | `Shl` |
<<= | `var <<= expr` | Left-shift and assignment | `ShlAssign` |
< | `expr < expr` | Less than comparison | `PartialOrd` |
<= | `expr <= expr` | Less than or equal to comparison | `PartialOrd` |
= | `var = expr`, `ident = type` | Assignment/equivalence | |
== | `expr == expr` | Equality comparison | `PartialEq` |
=> | `pat => expr` | Part of match arm syntax | |
> | `expr > expr` | Greater than comparison | `PartialOrd` |
>= | `expr >= expr` | Greater than or equal to comparison | `PartialOrd` |
>> | `expr >> expr` | Right-shift | `Shr` |
>>= | `var >>= expr` | Right-shift and assignment | `ShrAssign` |
@ | `ident @ pat` | Pattern binding | |
^ | `expr ^ expr` | Bitwise exclusive OR | `BitXor` |
^= | `var ^= expr` | Bitwise exclusive OR and assignment | `BitXorAssign` |
<code>|</code> | <code>pat | pat</code> | Pattern alternatives | |
<code>|</code> | <code>expr | expr</code> | Bitwise OR | `BitOr` |
<code>|=</code> | <code>var |= expr</code> | Bitwise OR and assignment | `BitOrAssign` |
<code>||</code> | <code>expr || expr</code> | Short-circuiting logical OR | |
? | `expr?` | Error propagation |
Non-operator Symbols
The following list contains all symbols that don’t function as operators; that is, they don’t behave like a function or method call.
Table B-2 shows symbols that appear on their own and are valid in a variety of locations.
Table B-2: Stand-Alone Syntax
Symbol | Explanation |
---|---|
'ident | Named lifetime or loop label |
...u8 , ...i32 , ...f64 , ...usize , etc. | Numeric literal of specific type |
"..." | String literal |
`r"..."`, `r#"..."#`, `r##"..."##`, etc. | Raw string literal, escape characters not processed |
`b"..."` | Byte string literal; constructs an array of bytes instead of a string |
`br"..."`, `br#"..."#`, `br##"..."##`, etc. | Raw byte string literal, combination of raw and byte string literal |
'...' | Character literal |
`b'...'` | ASCII byte literal |
<code>|...| expr</code> | Closure |
! | Always empty bottom type for diverging functions |
`_` | “Ignored” pattern binding; also used to make integer literals readable |
Table B-3 shows symbols that appear in the context of a path through the module hierarchy to an item.
Table B-3: Path-Related Syntax
Symbol | Explanation |
---|---|
`ident::ident` | Namespace path |
::path | Path relative to the crate root (i.e., an explicitly absolute path) |
`self::path` | Path relative to the current module (i.e., an explicitly relative path). |
`super::path` | Path relative to the parent of the current module |
`type::ident`, <type as trait>::ident | Associated constants, functions, and types |
<type>::... | Associated item for a type that cannot be directly named (e.g., <&T>::... , <\[T\]>::... , etc.) |
`trait::method(...)` | Disambiguating a method call by naming the trait that defines it |
`type::method(...)` | Disambiguating a method call by naming the type for which it’s defined |
<type as trait>::method(...) | Disambiguating a method call by naming the trait and type |
Table B-4 shows symbols that appear in the context of using generic type parameters.
Table B-4: Generics
Symbol | Explanation |
---|---|
`path<...>` | Specifies parameters to generic type in a type (e.g., `Vec<u8>`) |
`path::<...>`, `method::<...>` | Specifies parameters to generic type, function, or method in an expression; often referred to as turbofish (e.g., "42".parse::<i32>() ) |
`fn ident<...> ...` | Define generic function |
`struct ident<...> ...` | Define generic structure |
`enum ident<...> ...` | Define generic enumeration |
`impl<...> ...` | Define generic implementation |
`for<...> type` | Higher-ranked lifetime bounds |
`type<ident=type>` | A generic type where one or more associated types have specific assignments (e.g., `Iterator<Item=T>`) |
Table B-5 shows symbols that appear in the context of constraining generic type parameters with trait bounds.
Table B-5: Trait Bound Constraints
Symbol | Explanation |
---|---|
`T: U` | Generic parameter `T` constrained to types that implement `U` |
`T: 'a` | Generic type `T` must outlive lifetime 'a (meaning the type cannot transitively contain any references with lifetimes shorter than 'a ) |
`T: 'static` | Generic type `T` contains no borrowed references other than 'static ones |
'b: 'a | Generic lifetime 'b must outlive lifetime 'a |
`T: ?Sized` | Allow generic type parameter to be a dynamically sized type |
'a + trait , `trait + trait` | Compound type constraint |
Table B-6 shows symbols that appear in the context of calling or defining macros and specifying attributes on an item.
Table B-6: Macros and Attributes
Symbol | Explanation |
---|---|
#\[meta\] | Outer attribute |
#!\[meta\] | Inner attribute |
$ident | Macro substitution |
$ident:kind | Macro capture |
$(…)… | Macro repetition |
`ident!(...)`, `ident!{...}`, `ident![...]` | Macro invocation |
Table B-7 shows symbols that create comments.
Table B-7: Comments
Symbol | Explanation |
---|---|
// | Line comment |
//! | Inner line doc comment |
/// | Outer line doc comment |
/*...*/ | Block comment |
/*!...*/ | Inner block doc comment |
/**...*/ | Outer block doc comment |
Table B-8 shows symbols that appear in the context of using tuples.
Table B-8: Tuples
Symbol | Explanation |
---|---|
() | Empty tuple (aka unit), both literal and type |
(expr) | Parenthesized expression |
(expr,) | Single-element tuple expression |
(type,) | Single-element tuple type |
(expr, ...) | Tuple expression |
(type, ...) | Tuple type |
`expr(expr, ...)` | Function call expression; also used to initialize tuple `struct`s and tuple `enum` variants |
`expr.0`, `expr.1`, etc. | Tuple indexing |
Table B-9 shows the contexts in which curly braces are used.
Table B-9: Curly Brackets
Context | Explanation |
---|---|
{...} | Block expression |
`Type {...}` | `struct` literal |
Table B-10 shows the contexts in which square brackets are used.
Table B-10: Square Brackets
Context | Explanation |
---|---|
\[...\] | Array literal |
\[expr; len\] | Array literal containing `len` copies of `expr` |
\[type; len\] | Array type containing `len` instances of `type` |
`expr[expr]` | Collection indexing. Overloadable (`Index`, `IndexMut`) |
`expr[..]`, `expr[a..]`, `expr[..b]`, `expr[a..b]` | Collection indexing pretending to be collection slicing, using `Range`, `RangeFrom`, `RangeTo`, or `RangeFull` as the “index” |
Appendix C: Derivable Traits
Debug
for Programmer OutputPartialEq
andEq
for Equality ComparisonsPartialOrd
andOrd
for Ordering ComparisonsClone
andCopy
for Duplicating ValuesHash
for Mapping a Value to a Value of Fixed SizeDefault
for Default Values
In various places in the book, we’ve discussed the derive
attribute, which
you can apply to a struct or enum definition. The derive
attribute generates
code that will implement a trait with its own default implementation on the
type you’ve annotated with the derive
syntax.
In this appendix, we provide a reference of all the traits in the standard
library that you can use with derive
. Each section covers:
- What operators and methods deriving this trait will enable
- What the implementation of the trait provided by
derive
does - What implementing the trait signifies about the type
- The conditions in which you’re allowed or not allowed to implement the trait
- Examples of operations that require the trait
If you want different behavior from that provided by the derive
attribute,
consult the standard library documentation
for each trait for details of how to manually implement them.
These traits listed here are the only ones defined by the standard library that
can be implemented on your types using derive
. Other traits defined in the
standard library don’t have sensible default behavior, so it’s up to you to
implement them in the way that makes sense for what you’re trying to accomplish.
An example of a trait that can’t be derived is Display
, which handles
formatting for end users. You should always consider the appropriate way to
display a type to an end user. What parts of the type should an end user be
allowed to see? What parts would they find relevant? What format of the data
would be most relevant to them? The Rust compiler doesn’t have this insight, so
it can’t provide appropriate default behavior for you.
The list of derivable traits provided in this appendix is not comprehensive:
libraries can implement derive
for their own traits, making the list of
traits you can use derive
with truly open-ended. Implementing derive
involves using a procedural macro, which is covered in the
“Macros” section of Chapter 19.
Debug
for Programmer Output
The Debug
trait enables debug formatting in format strings, which you
indicate by adding :?
within {}
placeholders.
The Debug
trait allows you to print instances of a type for debugging
purposes, so you and other programmers using your type can inspect an instance
at a particular point in a program’s execution.
The Debug
trait is required, for example, in use of the assert_eq!
macro.
This macro prints the values of instances given as arguments if the equality
assertion fails so programmers can see why the two instances weren’t equal.
PartialEq
and Eq
for Equality Comparisons
The PartialEq
trait allows you to compare instances of a type to check for
equality and enables use of the ==
and !=
operators.
Deriving PartialEq
implements the eq
method. When PartialEq
is derived on
structs, two instances are equal only if all fields are equal, and the
instances are not equal if any fields are not equal. When derived on enums,
each variant is equal to itself and not equal to the other variants.
The PartialEq
trait is required, for example, with the use of the
assert_eq!
macro, which needs to be able to compare two instances of a type
for equality.
The Eq
trait has no methods. Its purpose is to signal that for every value of
the annotated type, the value is equal to itself. The Eq
trait can only be
applied to types that also implement PartialEq
, although not all types that
implement PartialEq
can implement Eq
. One example of this is floating point
number types: the implementation of floating point numbers states that two
instances of the not-a-number (NaN
) value are not equal to each other.
An example of when Eq
is required is for keys in a HashMap<K, V>
so the
HashMap<K, V>
can tell whether two keys are the same.
PartialOrd
and Ord
for Ordering Comparisons
The PartialOrd
trait allows you to compare instances of a type for sorting
purposes. A type that implements PartialOrd
can be used with the <
, >
,
<=
, and >=
operators. You can only apply the PartialOrd
trait to types
that also implement PartialEq
.
Deriving PartialOrd
implements the partial_cmp
method, which returns an
Option<Ordering>
that will be None
when the values given don’t produce an
ordering. An example of a value that doesn’t produce an ordering, even though
most values of that type can be compared, is the not-a-number (NaN
) floating
point value. Calling partial_cmp
with any floating point number and the NaN
floating point value will return None
.
When derived on structs, PartialOrd
compares two instances by comparing the
value in each field in the order in which the fields appear in the struct
definition. When derived on enums, variants of the enum declared earlier in the
enum definition are considered less than the variants listed later.
The PartialOrd
trait is required, for example, for the gen_range
method
from the rand
crate that generates a random value in the range specified by a
range expression.
The Ord
trait allows you to know that for any two values of the annotated
type, a valid ordering will exist. The Ord
trait implements the cmp
method,
which returns an Ordering
rather than an Option<Ordering>
because a valid
ordering will always be possible. You can only apply the Ord
trait to types
that also implement PartialOrd
and Eq
(and Eq
requires PartialEq
). When
derived on structs and enums, cmp
behaves the same way as the derived
implementation for partial_cmp
does with PartialOrd
.
An example of when Ord
is required is when storing values in a BTreeSet<T>
,
a data structure that stores data based on the sort order of the values.
Clone
and Copy
for Duplicating Values
The Clone
trait allows you to explicitly create a deep copy of a value, and
the duplication process might involve running arbitrary code and copying heap
data. See the “Ways Variables and Data Interact:
Clone” section in
Chapter 4 for more information on Clone
.
Deriving Clone
implements the clone
method, which when implemented for the
whole type, calls clone
on each of the parts of the type. This means all the
fields or values in the type must also implement Clone
to derive Clone
.
An example of when Clone
is required is when calling the to_vec
method on a
slice. The slice doesn’t own the type instances it contains, but the vector
returned from to_vec
will need to own its instances, so to_vec
calls
clone
on each item. Thus, the type stored in the slice must implement Clone
.
The Copy
trait allows you to duplicate a value by only copying bits stored on
the stack; no arbitrary code is necessary. See the “Stack-Only Data:
Copy” section in Chapter 4 for more
information on Copy
.
The Copy
trait doesn’t define any methods to prevent programmers from
overloading those methods and violating the assumption that no arbitrary code
is being run. That way, all programmers can assume that copying a value will be
very fast.
You can derive Copy
on any type whose parts all implement Copy
. A type that
implements Copy
must also implement Clone
, because a type that implements
Copy
has a trivial implementation of Clone
that performs the same task as
Copy
.
The Copy
trait is rarely required; types that implement Copy
have
optimizations available, meaning you don’t have to call clone
, which makes
the code more concise.
Everything possible with Copy
you can also accomplish with Clone
, but the
code might be slower or have to use clone
in places.
Hash
for Mapping a Value to a Value of Fixed Size
The Hash
trait allows you to take an instance of a type of arbitrary size and
map that instance to a value of fixed size using a hash function. Deriving
Hash
implements the hash
method. The derived implementation of the hash
method combines the result of calling hash
on each of the parts of the type,
meaning all fields or values must also implement Hash
to derive Hash
.
An example of when Hash
is required is in storing keys in a HashMap<K, V>
to store data efficiently.
Default
for Default Values
The Default
trait allows you to create a default value for a type. Deriving
Default
implements the default
function. The derived implementation of the
default
function calls the default
function on each part of the type,
meaning all fields or values in the type must also implement Default
to
derive Default
.
The Default::default
function is commonly used in combination with the struct
update syntax discussed in the “Creating Instances From Other Instances With
Struct Update
Syntax”
section in Chapter 5. You can customize a few fields of a struct and then
set and use a default value for the rest of the fields by using
..Default::default()
.
The Default
trait is required when you use the method unwrap_or_default
on
Option<T>
instances, for example. If the Option<T>
is None
, the method
unwrap_or_default
will return the result of Default::default
for the type
T
stored in the Option<T>
.
Useful Development Tools
- Automatic Formatting with
rustfmt
- Fix Your Code with
rustfix
- More Lints with Clippy
- IDE Integration Using
rust-analyzer
In this appendix, we talk about some useful development tools that the Rust project provides. We’ll look at automatic formatting, quick ways to apply warning fixes, a linter, and integrating with IDEs.
Automatic Formatting with rustfmt
The rustfmt
tool reformats your code according to the community code style.
Many collaborative projects use rustfmt
to prevent arguments about which
style to use when writing Rust: everyone formats their code using the tool.
To install rustfmt
, enter the following:
$ rustup component add rustfmt
This command gives you rustfmt
and cargo-fmt
, similar to how Rust gives you
both rustc
and cargo
. To format any Cargo project, enter the following:
$ cargo fmt
Running this command reformats all the Rust code in the current crate. This
should only change the code style, not the code semantics. For more information
on rustfmt
, see its documentation.
Fix Your Code with rustfix
The rustfix tool is included with Rust installations and can automatically fix compiler warnings that have a clear way to correct the problem that’s likely what you want. It’s likely you’ve seen compiler warnings before. For example, consider this code:
Filename: src/main.rs
fn do_something() {} fn main() { for i in 0..100 { do_something(); } }
Here, we’re calling the do_something
function 100 times, but we never use the
variable i
in the body of the for
loop. Rust warns us about that:
$ cargo build
Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: unused variable: `i`
--> src/main.rs:4:9
|
4 | for i in 0..100 {
| ^ help: consider using `_i` instead
|
= note: #[warn(unused_variables)] on by default
Finished dev [unoptimized + debuginfo] target(s) in 0.50s
The warning suggests that we use _i
as a name instead: the underscore
indicates that we intend for this variable to be unused. We can automatically
apply that suggestion using the rustfix
tool by running the command cargo fix
:
$ cargo fix
Checking myprogram v0.1.0 (file:///projects/myprogram)
Fixing src/main.rs (1 fix)
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
When we look at src/main.rs again, we’ll see that cargo fix
has changed the
code:
Filename: src/main.rs
fn do_something() {} fn main() { for _i in 0..100 { do_something(); } }
The for
loop variable is now named _i
, and the warning no longer appears.
You can also use the cargo fix
command to transition your code between
different Rust editions. Editions are covered in Appendix E.
More Lints with Clippy
The Clippy tool is a collection of lints to analyze your code so you can catch common mistakes and improve your Rust code.
To install Clippy, enter the following:
$ rustup component add clippy
To run Clippy’s lints on any Cargo project, enter the following:
$ cargo clippy
For example, say you write a program that uses an approximation of a mathematical constant, such as pi, as this program does:
Filename: src/main.rs
fn main() { let x = 3.1415; let r = 8.0; println!("the area of the circle is {}", x * r * r); }
Running cargo clippy
on this project results in this error:
error: approximate value of `f{32, 64}::consts::PI` found
--> src/main.rs:2:13
|
2 | let x = 3.1415;
| ^^^^^^
|
= note: `#[deny(clippy::approx_constant)]` on by default
= help: consider using the constant directly
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
This error lets you know that Rust already has a more precise PI
constant
defined, and that your program would be more correct if you used the constant
instead. You would then change your code to use the PI
constant. The
following code doesn’t result in any errors or warnings from Clippy:
Filename: src/main.rs
fn main() { let x = std::f64::consts::PI; let r = 8.0; println!("the area of the circle is {}", x * r * r); }
For more information on Clippy, see its documentation.
IDE Integration Using rust-analyzer
To help IDE integration, the Rust community recommends using
rust-analyzer
. This tool is a set of
compiler-centric utilities that speaks the Language Server Protocol, which is a specification for IDEs and programming languages to
communicate with each other. Different clients can use rust-analyzer
, such as
the Rust analyzer plug-in for Visual Studio Code.
Visit the rust-analyzer
project’s home page
for installation instructions, then install the language server support in your
particular IDE. Your IDE will gain abilities such as autocompletion, jump to
definition, and inline errors.
Random Code Samples
Greatest Common Denominator
Uses the Euclidean Algorithm to get Greatest Common Denominator
use std::str::FromStr; use std::env; fn gcd(mut n: u64, mut m: u64) -> u64 { // Make sure that either number isn't 0 assert!(n != 0 && m != 0); // Run a loop while the remainder `m` is not 0 while m != 0 { // Make sure we are always trying to get the divsor of // the smaller number if m < n { // initialize a temp variable let temp = m; m = n; n = temp; } // Get the divisor m = m % n; } // return n aka the greatest common denominator n } #[test] fn test_gcd() { assert_eq!(gcd(14, 15), 1); assert_eq!(gcd(8, 4), 4); } fn main() { // Initiate the vector to put in you CLI args let mut numbers = vec![]; // Parse the CLI args // The first arg is aleays the name of the program so skip it for arg in env::args().skip(1) { numbers.push(u64::from_str(&arg) // Parse the arg and throw an error iff there is an issue .expect("error parsing argument")); } // Throw error and exit if there are no args if numbers.len() == 0 { eprintln!("Usage: gcd NUMBER ..."); std::process::exit(1); } // Iterate through the args let mut d = numbers[0]; for m in &numbers[1..] { d = gcd(d, *m); } // Print the gcd println!("The greatest common divisor of {:?} is {}", numbers, d); }
Serving a Web Page
Serves the gcd()
function from GCD.
use actix_web::{web, App, HttpResponse, HttpServer}; use serde::Deserialize; #[derive(Deserialize)] struct GcdParameters { n: u64, m: u64, } fn main() { let server = HttpServer::new(|| { App::new() .route("/", web::get().to(get_index)) }); println!("Serving on http://localhost:3000...") server .bind("0.0.0.0:3000").expect("error binding to the server address") .run().expect("error running server"); } fn get_index() -> HttpResponse { HttpResponse::Ok() .content_type("text/html") .body( r#" <title>GCD Calculator</title> <form action="/gcd" method="post"> <input type="text" name="n"/> <input type="text" name="m"/> <button type="submit">Compute GCD</button> </form> "#, ) } fn gcd(mut n: u64, mut m: u64) -> u64 { // Make sure that either number isn't 0 assert!(n != 0 && m != 0); // Run a loop while the remainder `m` is not 0 while m != 0 { // Make sure we are always trying to get the divsor of // the smaller number if m < n { // initialize a temp variable let temp = m; m = n; n = temp; } // Get the divisor m = m % n; } // return n aka the greatest common denominator n } fn post_gcd(form: web::Form<GcdParameters>) -> HttpResponse { if form.n ==0 || form.m == 0 { return HttpResponse::BadRequest() .content_type("text/html") .body("Computing the GCD with 0 is boring."); } let response = format!("The greatest common divisor of the numbers {} and ) }
Mandelbrot
Mandelbrot code sample
// Use the num crate to bring in complex numbers use num::Complex; use std::str::FromStr; use image::ColorType; use image::png::PNGEncoder; use std::fs::File; use std::env; fn complex_square_add_loop(c: Complex<f64>) { let mut z = Complex { re: 0.0, im: 0.0 }; loop { z = z * z + c; } } fn escape_time(c: Complex<f64>, limit: usize) -> Option<usize> { let mut z = Complex { re: 0.0, im: 0.0}; for i in 0..limit { if z.norm_sqr() > 4.0 { return Some(i); } z = z * z + c; } None } fn parse_pair<T: FromStr>(s: &str, separator: char) -> Option<(T, T)> { match s.find(separator) { None => None, Some(Index) => { match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) { (Ok(l), Ok(r)) => Some((l, r)), _ => None } } } } #[test] fn test_parse_pair() { assert_eq!(parse_pair::<i32>("", ','), None); } fn pixel_to_point(bounds: (usize, usize), pixel: (usize, usize), upper_left: Complex<f64>, lower_right: Complex<f64>) -> Complex<f64> { let (width, height) = (lower_right.re - upper_left.re, upper_left.im - lower_right.im); Complex { re: upper_left.re + pixel.0 as f64 * width / bounds.0 as f64, im: upper_left.im - pixel.1 as f64 * height / bounds.1 as f64 } } #[test] fn test_pixel_to_point() { assert_eq!(pixel_to_point((100, 200), (25, 175), Complex { re: -1.0, im: 1.0 }, Complex { re: 1.0, im: -1.0 }), Complex { re: -0.5, im: -0.75 }); } fn render(pixels: &mut[u8], bounds: (usize, usize), upper_left: Complex<f64>, lower_right: Complex<f64>) { assert!(pizels.len() == bounds.0 * bounds.1); for row in 0..bounds.1 { for column in 0..bounds.0 { let point = pixel_to_point(bounds, (column, row), upper_left, lower_right); pixels[row * bounds.0 + column] = match escape_time(point, 255) { None => 0, Some(count) => 255 - count as u8 }; } } } fn write_image(filename: &str, pixels: &[u8], bounds: (usize, usize)) -> Result<(), std::io::Error> { let output = File::create(filename)?; let encoder = PNGEncoder::new(output); encoder.encode(pixels, bounds.0 as u32, bounds.1 as u32, ColorType::Gray(8))?; Ok(()) } fn main() { let args: Vec<String> = env::args().collect(); if args.len() != 5 { eprintln!("Usage: {} FILE PIXELS UPPERLEFT LOWERRIGHT", args[0]); eprintln!("Example: {} mandel.png 1000X750 -1.20,0.35 -1,0.20", args[0]); std::process::exit(1); } let bounds = parse_pair(&args[2], 'x') .expect("error parsing image dimensions"); let upper_left = parse_complex(&args[3]) .expect("error parsing upper left corner point"); let lower_right = parse_complex(&args[4]) .expect("error parsing lower right corner"); let mut pixels = vec![0; bounds.0 * bounds.1]; render(&mut pixels, bounds, upper_left, lower_right); write_image(&args[1], &pixels, bounds) .expect("error writing PNG file"); }