Experiment Introduction
Welcome to the Rust Book experiment, and thank you for your participation! First, we want to introduce you to the new mechanics of this experiment.
1. Quizzes
The main mechanic is quizzes: each page has a few quizzes about the page’s content. We have two rules about quizzes for this experiment:
- Take a quiz as soon as you get to it.
- Do not skip a quiz.
(We don’t enforce these rules, but please follow them!)
Every quiz looks like the one below. Try it out by clicking “Start”.
If you get a question incorrect, you can choose to either retry the quiz, or see the correct answers. We encourage you to retry the quiz until you get 100% — feel free to review the content before retrying the quiz. Note that once you see the correct answers, you cannot retry the quiz.
If you spot an issue in a quiz or other part of the book, you can file an issue on our Github repository: https://github.com/cognitive-engineering-lab/rust-book
2. Highlighting
Another mechanic is highlighting: you can select any piece of text, and either highlight it or leave a comment about it. Once you select some text, click the ✏️ button, and leave an optional comment.
👉 Try highlighting this text! 👈
You can use highlights to save information for yourself. You can also use highlights to give us feedback — for example, if you think a section of content is confusing, you can let us know.
Note: your highlights will disappear if we change the content that you’ve highlighted. Also, your highlights are stored as a cookie. If you block cookies or change browsers, then you won’t see your previous highlights.
3. …and more!
The book’s content may change as you go through the experiment. We will update this page as we add new features. Here’s the changelog:
- September 26, 2024
- Chris Krycho’s chapter on async Rust has been added, along with new quiz questions.
- February 16, 2023
- A new chapter on ownership has replaced the previous Chapter 4.
- January 18, 2023
- Questions have been added for the remaining chapters of the book.
- December 15, 2022
- New sections called “Ownership Inventory” have been added throughout the book with challenging ownership-related questions.
- November 7, 2022
- Only questions with incorrect answers will be shown on a retry.
- Most multiple-choice questions will have their choices randomized.
- Some questions will now prompt for your reasoning.
- Many questions have been updated based on your feedback. Keep it coming!
Interested in participating in other experiments about making Rust easier to learn? Please sign up here: https://forms.gle/U3jEUkb2fGXykp1DA
4. Publications
Thus far, this experiment has led to two open-access publications. Check them out if you’re interested to see the academic research behind this book:
-
Profiling Programming Language Learning
Will Crichton and Shriram Krishnamurthi. OOPSLA 2024. (Distinguished Paper!) -
A Grounded Conceptual Model for Ownership Types in Rust
Will Crichton, Gavin Gray, and Shriram Krishnamurthi. OOPSLA 2023.
5. Acknowledgments
Niko Matsakis and Amazon Web Services provided funding for this experiment. Carol Nichols and the Rust Foundation helped publicize the experiment. TRPL is the product of many people’s hard work before we started this experiment.
The Rust Programming Language
by Steve Klabnik and Carol Nichols, with contributions from the Rust Community
(and with experimental modifications!)
This version of the text assumes you’re using Rust 1.81.0 (released 2024-09-04)
or later. See the “Installation” section of Chapter 1
to install or update Rust. Run rustc --version
to see your Rust version.
The HTML format is available online at
https://doc.rust-lang.org/stable/book/
and offline with installations of Rust made with rustup
; run rustup doc --book
to open.
Several community translations are also available.
This text is available in paperback and ebook format from No Starch Press.
Foreword
It wasn’t always so clear, but the Rust programming language is fundamentally about empowerment: no matter what kind of code you are writing now, Rust empowers you to reach farther, to program with confidence in a wider variety of domains than you did before.
Take, for example, “systems-level” work that deals with low-level details of memory management, data representation, and concurrency. Traditionally, this realm of programming is seen as arcane, accessible only to a select few who have devoted the necessary years learning to avoid its infamous pitfalls. And even those who practice it do so with caution, lest their code be open to exploits, crashes, or corruption.
Rust breaks down these barriers by eliminating the old pitfalls and providing a friendly, polished set of tools to help you along the way. Programmers who need to “dip down” into lower-level control can do so with Rust, without taking on the customary risk of crashes or security holes, and without having to learn the fine points of a fickle toolchain. Better yet, the language is designed to guide you naturally towards reliable code that is efficient in terms of speed and memory usage.
Programmers who are already working with low-level code can use Rust to raise their ambitions. For example, introducing parallelism in Rust is a relatively low-risk operation: the compiler will catch the classical mistakes for you. And you can tackle more aggressive optimizations in your code with the confidence that you won’t accidentally introduce crashes or vulnerabilities.
But Rust isn’t limited to low-level systems programming. It’s expressive and ergonomic enough to make CLI apps, web servers, and many other kinds of code quite pleasant to write — you’ll find simple examples of both later in the book. Working with Rust allows you to build skills that transfer from one domain to another; you can learn Rust by writing a web app, then apply those same skills to target your Raspberry Pi.
This book fully embraces the potential of Rust to empower its users. It’s a friendly and approachable text intended to help you level up not just your knowledge of Rust, but also your reach and confidence as a programmer in general. So dive in, get ready to learn—and welcome to the Rust community!
— Nicholas Matsakis and Aaron Turon
Introduction
Note: This edition of the book is the same as The Rust Programming Language available in print and ebook format from No Starch Press.
Welcome to The Rust Programming Language, an introductory book about Rust. The Rust programming language helps you write faster, more reliable software. High-level ergonomics and low-level control are often at odds in programming language design; Rust challenges that conflict. Through balancing powerful technical capacity and a great developer experience, Rust gives you the option to control low-level details (such as memory usage) without all the hassle traditionally associated with such control.
Who Rust Is For
Rust is ideal for many people for a variety of reasons. Let’s look at a few of the most important groups.
Teams of Developers
Rust is proving to be a productive tool for collaborating among large teams of developers with varying levels of systems programming knowledge. Low-level code is prone to various subtle bugs, which in most other languages can be caught only through extensive testing and careful code review by experienced developers. In Rust, the compiler plays a gatekeeper role by refusing to compile code with these elusive bugs, including concurrency bugs. By working alongside the compiler, the team can spend their time focusing on the program’s logic rather than chasing down bugs.
Rust also brings contemporary developer tools to the systems programming world:
- Cargo, the included dependency manager and build tool, makes adding, compiling, and managing dependencies painless and consistent across the Rust ecosystem.
- The Rustfmt formatting tool ensures a consistent coding style across developers.
- The rust-analyzer powers Integrated Development Environment (IDE) integration for code completion and inline error messages.
By using these and other tools in the Rust ecosystem, developers can be productive while writing systems-level code.
Students
Rust is for students and those who are interested in learning about systems concepts. Using Rust, many people have learned about topics like operating systems development. The community is very welcoming and happy to answer student questions. Through efforts such as this book, the Rust teams want to make systems concepts more accessible to more people, especially those new to programming.
Companies
Hundreds of companies, large and small, use Rust in production for a variety of tasks, including command line tools, web services, DevOps tooling, embedded devices, audio and video analysis and transcoding, cryptocurrencies, bioinformatics, search engines, Internet of Things applications, machine learning, and even major parts of the Firefox web browser.
Open Source Developers
Rust is for people who want to build the Rust programming language, community, developer tools, and libraries. We’d love to have you contribute to the Rust language.
People Who Value Speed and Stability
Rust is for people who crave speed and stability in a language. By speed, we mean both how quickly Rust code can run and the speed at which Rust lets you write programs. The Rust compiler’s checks ensure stability through feature additions and refactoring. This is in contrast to the brittle legacy code in languages without these checks, which developers are often afraid to modify. By striving for zero-cost abstractions, higher-level features that compile to lower-level code as fast as code written manually, Rust endeavors to make safe code be fast code as well.
The Rust language hopes to support many other users as well; those mentioned here are merely some of the biggest stakeholders. Overall, Rust’s greatest ambition is to eliminate the trade-offs that programmers have accepted for decades by providing safety and productivity, speed and ergonomics. Give Rust a try and see if its choices work for you.
Who This Book Is For
This book assumes that you’ve written code in another programming language but doesn’t make any assumptions about which one. We’ve tried to make the material broadly accessible to those from a wide variety of programming backgrounds. We don’t spend a lot of time talking about what programming is or how to think about it. If you’re entirely new to programming, you would be better served by reading a book that specifically provides an introduction to programming.
How to Use This Book
In general, this book assumes that you’re reading it in sequence from front to back. Later chapters build on concepts in earlier chapters, and earlier chapters might not delve into details on a particular topic but will revisit the topic in a later chapter.
You’ll find two kinds of chapters in this book: concept chapters and project chapters. In concept chapters, you’ll learn about an aspect of Rust. In project chapters, we’ll build small programs together, applying what you’ve learned so far. Chapters 2, 12, and 20 are project chapters; the rest are concept chapters.
Chapter 1 explains how to install Rust, how to write a “Hello, world!” program, and how to use Cargo, Rust’s package manager and build tool. Chapter 2 is a hands-on introduction to writing a program in Rust, having you build up a number guessing game. Here we cover concepts at a high level, and later chapters will provide additional detail. If you want to get your hands dirty right away, Chapter 2 is the place for that. Chapter 3 covers Rust features that are similar to those of other programming languages, and in Chapter 4 you’ll learn about Rust’s ownership system. If you’re a particularly meticulous learner who prefers to learn every detail before moving on to the next, you might want to skip Chapter 2 and go straight to Chapter 3, returning to Chapter 2 when you’d like to work on a project applying the details you’ve learned.
Chapter 5 discusses structs and methods, and Chapter 6 covers enums, match
expressions, and the if let
control flow construct. You’ll use structs and
enums to make custom types in Rust.
In Chapter 7, you’ll learn about Rust’s module system and about privacy rules for organizing your code and its public Application Programming Interface (API). Chapter 8 discusses some common collection data structures that the standard library provides, such as vectors, strings, and hash maps. Chapter 9 explores Rust’s error-handling philosophy and techniques.
Chapter 10 digs into generics, traits, and lifetimes, which give you the power
to define code that applies to multiple types. Chapter 11 is all about testing,
which even with Rust’s safety guarantees is necessary to ensure your program’s
logic is correct. In Chapter 12, we’ll build our own implementation of a subset
of functionality from the grep
command line tool that searches for text
within files. For this, we’ll use many of the concepts we discussed in the
previous chapters.
Chapter 13 explores closures and iterators: features of Rust that come from functional programming languages. In Chapter 14, we’ll examine Cargo in more depth and talk about best practices for sharing your libraries with others. Chapter 15 discusses smart pointers that the standard library provides and the traits that enable their functionality.
In Chapter 16, we’ll walk through different models of concurrent programming and talk about how Rust helps you to program in multiple threads fearlessly. Chapter 17 looks at how Rust idioms compare to object-oriented programming principles you might be familiar with.
Chapter 18 is a reference on patterns and pattern matching, which are powerful ways of expressing ideas throughout Rust programs. Chapter 19 contains a smorgasbord of advanced topics of interest, including unsafe Rust, macros, and more about lifetimes, traits, types, functions, and closures.
In Chapter 20, we’ll complete a project in which we’ll implement a low-level multithreaded web server!
Finally, some appendices contain useful information about the language in a more reference-like format. Appendix A covers Rust’s keywords, Appendix B covers Rust’s operators and symbols, Appendix C covers derivable traits provided by the standard library, Appendix D covers some useful development tools, and Appendix E explains Rust editions. In Appendix F, you can find translations of the book, and in Appendix G we’ll cover how Rust is made and what nightly Rust is.
There is no wrong way to read this book: if you want to skip ahead, go for it! You might have to jump back to earlier chapters if you experience any confusion. But do whatever works for you.
An important part of the process of learning Rust is learning how to read the error messages the compiler displays: these will guide you toward working code. As such, we’ll provide many examples that don’t compile along with the error message the compiler will show you in each situation. Know that if you enter and run a random example, it may not compile! Make sure you read the surrounding text to see whether the example you’re trying to run is meant to error. Ferris will also help you distinguish code that isn’t meant to work:
Ferris | Meaning |
---|---|
This code does not compile! | |
This code panics! | |
This code does not produce the desired behavior. |
In most situations, we’ll lead you to the correct version of any code that doesn’t compile.
Source Code
The source files from which this book is generated can be found on GitHub.
Getting Started
Let’s start your Rust journey! There’s a lot to learn, but every journey starts somewhere. In this chapter, we’ll discuss:
- Installing Rust on Linux, macOS, and Windows
- Writing a program that prints
Hello, world!
- Using
cargo
, Rust’s package manager and build system
Installation
The first step is to install Rust. We’ll download Rust through rustup
, a
command line tool for managing Rust versions and associated tools. You’ll need
an internet connection for the download.
Note: If you prefer not to use rustup
for some reason, please see the
Other Rust Installation Methods page for more options.
The following steps install the latest stable version of the Rust compiler. Rust’s stability guarantees ensure that all the examples in the book that compile will continue to compile with newer Rust versions. The output might differ slightly between versions because Rust often improves error messages and warnings. In other words, any newer, stable version of Rust you install using these steps should work as expected with the content of this book.
Command Line Notation
In this chapter and throughout the book, we’ll show some commands used in the
terminal. Lines that you should enter in a terminal all start with $
. You
don’t need to type the $
character; it’s the command line prompt shown to
indicate the start of each command. Lines that don’t start with $
typically
show the output of the previous command. Additionally, PowerShell-specific
examples will use >
rather than $
.
Installing rustup
on Linux or macOS
If you’re using Linux or macOS, open a terminal and enter the following command:
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
The command downloads a script and starts the installation of the rustup
tool, which installs the latest stable version of Rust. You might be prompted
for your password. If the install is successful, the following line will appear:
Rust is installed now. Great!
You will also need a linker, which is a program that Rust uses to join its compiled outputs into one file. It is likely you already have one. If you get linker errors, you should install a C compiler, which will typically include a linker. A C compiler is also useful because some common Rust packages depend on C code and will need a C compiler.
On macOS, you can get a C compiler by running:
$ xcode-select --install
Linux users should generally install GCC or Clang, according to their
distribution’s documentation. For example, if you use Ubuntu, you can install
the build-essential
package.
Installing rustup
on Windows
On Windows, go to https://www.rust-lang.org/tools/install and follow the instructions for installing Rust. At some point in the installation, you’ll be prompted to install Visual Studio. This provides a linker and the native libraries needed to compile programs. If you need more help with this step, see https://rust-lang.github.io/rustup/installation/windows-msvc.html
The rest of this book uses commands that work in both cmd.exe and PowerShell. If there are specific differences, we’ll explain which to use.
Troubleshooting
To check whether you have Rust installed correctly, open a shell and enter this line:
$ rustc --version
You should see the version number, commit hash, and commit date for the latest stable version that has been released, in the following format:
rustc x.y.z (abcabcabc yyyy-mm-dd)
If you see this information, you have installed Rust successfully! If you don’t
see this information, check that Rust is in your PATH
system variable as follows.
In Windows CMD, use:
> echo %PATH%
In PowerShell, use:
> echo $env:Path
In Linux and macOS, use:
$ echo $PATH
If that’s all correct and Rust still isn’t working, there are a number of places you can get help. Find out how to get in touch with other Rustaceans (a silly nickname we call ourselves) on the community page.
Updating and Uninstalling
Once Rust is installed via rustup
, updating to a newly released version is
easy. From your shell, run the following update script:
$ rustup update
To uninstall Rust and rustup
, run the following uninstall script from your
shell:
$ rustup self uninstall
Local Documentation
The installation of Rust also includes a local copy of the documentation so
that you can read it offline. Run rustup doc
to open the local documentation
in your browser.
Any time a type or function is provided by the standard library and you’re not sure what it does or how to use it, use the application programming interface (API) documentation to find out!
Hello, World!
Now that you’ve installed Rust, it’s time to write your first Rust program.
It’s traditional when learning a new language to write a little program that
prints the text Hello, world!
to the screen, so we’ll do the same here!
Note: This book assumes basic familiarity with the command line. Rust makes
no specific demands about your editing or tooling or where your code lives, so
if you prefer to use an integrated development environment (IDE) instead of
the command line, feel free to use your favorite IDE. Many IDEs now have some
degree of Rust support; check the IDE’s documentation for details. The Rust
team has been focusing on enabling great IDE support via rust-analyzer
. See
Appendix D for more details.
Creating a Project Directory
You’ll start by making a directory to store your Rust code. It doesn’t matter to Rust where your code lives, but for the exercises and projects in this book, we suggest making a projects directory in your home directory and keeping all your projects there.
Open a terminal and enter the following commands to make a projects directory and a directory for the “Hello, world!” project within the projects directory.
For Linux, macOS, and PowerShell on Windows, enter this:
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
For Windows CMD, enter this:
> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world
Writing and Running a Rust Program
Next, make a new source file and call it main.rs. Rust files always end with the .rs extension. If you’re using more than one word in your filename, the convention is to use an underscore to separate them. For example, use hello_world.rs rather than helloworld.rs.
Now open the main.rs file you just created and enter the code in Listing 1-1.
Save the file and go back to your terminal window in the ~/projects/hello_world directory. On Linux or macOS, enter the following commands to compile and run the file:
$ rustc main.rs
$ ./main
Hello, world!
On Windows, enter the command .\main.exe
instead of ./main
:
> rustc main.rs
> .\main.exe
Hello, world!
Regardless of your operating system, the string Hello, world!
should print to
the terminal. If you don’t see this output, refer back to the
“Troubleshooting” part of the Installation
section for ways to get help.
If Hello, world!
did print, congratulations! You’ve officially written a Rust
program. That makes you a Rust programmer—welcome!
Anatomy of a Rust Program
Let’s review this “Hello, world!” program in detail. Here’s the first piece of the puzzle:
fn main() { }
These lines define a function named main
. The main
function is special: it
is always the first code that runs in every executable Rust program. Here, the
first line declares a function named main
that has no parameters and returns
nothing. If there were parameters, they would go inside the parentheses ()
.
The function body is wrapped in {}
. Rust requires curly brackets around all
function bodies. It’s good style to place the opening curly bracket on the same
line as the function declaration, adding one space in between.
Note: If you want to stick to a standard style across Rust projects, you can
use an automatic formatter tool called rustfmt
to format your code in a
particular style (more on rustfmt
in
Appendix D). The Rust team has included this tool
with the standard Rust distribution, as rustc
is, so it should already be
installed on your computer!
The body of the main
function holds the following code:
#![allow(unused)] fn main() { println!("Hello, world!"); }
This line does all the work in this little program: it prints text to the screen. There are four important details to notice here.
First, Rust style is to indent with four spaces, not a tab.
Second, println!
calls a Rust macro. If it had called a function instead, it
would be entered as println
(without the !
). We’ll discuss Rust macros in
more detail in Chapter 19. For now, you just need to know that using a !
means that you’re calling a macro instead of a normal function and that macros
don’t always follow the same rules as functions.
Third, you see the "Hello, world!"
string. We pass this string as an argument
to println!
, and the string is printed to the screen.
Fourth, we end the line with a semicolon (;
), which indicates that this
expression is over and the next one is ready to begin. Most lines of Rust code
end with a semicolon.
Compiling and Running Are Separate Steps
You’ve just run a newly created program, so let’s examine each step in the process.
Before running a Rust program, you must compile it using the Rust compiler by
entering the rustc
command and passing it the name of your source file, like
this:
$ rustc main.rs
If you have a C or C++ background, you’ll notice that this is similar to gcc
or clang
. After compiling successfully, Rust outputs a binary executable.
On Linux, macOS, and PowerShell on Windows, you can see the executable by
entering the ls
command in your shell:
$ ls
main main.rs
On Linux and macOS, you’ll see two files. With PowerShell on Windows, you’ll see the same three files that you would see using CMD. With CMD on Windows, you would enter the following:
> dir /B %= the /B option says to only show the file names =%
main.exe
main.pdb
main.rs
This shows the source code file with the .rs extension, the executable file (main.exe on Windows, but main on all other platforms), and, when using Windows, a file containing debugging information with the .pdb extension. From here, you run the main or main.exe file, like this:
$ ./main # or .\main.exe on Windows
If your main.rs is your “Hello, world!” program, this line prints Hello, world!
to your terminal.
If you’re more familiar with a dynamic language, such as Ruby, Python, or JavaScript, you might not be used to compiling and running a program as separate steps. Rust is an ahead-of-time compiled language, meaning you can compile a program and give the executable to someone else, and they can run it even without having Rust installed. If you give someone a .rb, .py, or .js file, they need to have a Ruby, Python, or JavaScript implementation installed (respectively). But in those languages, you only need one command to compile and run your program. Everything is a trade-off in language design.
Just compiling with rustc
is fine for simple programs, but as your project
grows, you’ll want to manage all the options and make it easy to share your
code. Next, we’ll introduce you to the Cargo tool, which will help you write
real-world Rust programs.
Hello, Cargo!
Cargo is Rust’s build system and package manager. Most Rustaceans use this tool to manage their Rust projects because Cargo handles a lot of tasks for you, such as building your code, downloading the libraries your code depends on, and building those libraries. (We call the libraries that your code needs dependencies.)
The simplest Rust programs, like the one we’ve written so far, don’t have any dependencies. If we had built the “Hello, world!” project with Cargo, it would only use the part of Cargo that handles building your code. As you write more complex Rust programs, you’ll add dependencies, and if you start a project using Cargo, adding dependencies will be much easier to do.
Because the vast majority of Rust projects use Cargo, the rest of this book assumes that you’re using Cargo too. Cargo comes installed with Rust if you used the official installers discussed in the “Installation” section. If you installed Rust through some other means, check whether Cargo is installed by entering the following in your terminal:
$ cargo --version
If you see a version number, you have it! If you see an error, such as command not found
, look at the documentation for your method of installation to
determine how to install Cargo separately.
Creating a Project with Cargo
Let’s create a new project using Cargo and look at how it differs from our original “Hello, world!” project. Navigate back to your projects directory (or wherever you decided to store your code). Then, on any operating system, run the following:
$ cargo new hello_cargo
$ cd hello_cargo
The first command creates a new directory and project called hello_cargo. We’ve named our project hello_cargo, and Cargo creates its files in a directory of the same name.
Go into the hello_cargo directory and list the files. You’ll see that Cargo has generated two files and one directory for us: a Cargo.toml file and a src directory with a main.rs file inside.
It has also initialized a new Git repository along with a .gitignore file.
Git files won’t be generated if you run cargo new
within an existing Git
repository; you can override this behavior by using cargo new --vcs=git
.
Note: Git is a common version control system. You can change cargo new
to
use a different version control system or no version control system by using
the --vcs
flag. Run cargo new --help
to see the available options.
Open Cargo.toml in your text editor of choice. It should look similar to the code in Listing 1-2.
This file is in the TOML (Tom’s Obvious, Minimal Language) format, which is Cargo’s configuration format.
The first line, [package]
, is a section heading that indicates that the
following statements are configuring a package. As we add more information to
this file, we’ll add other sections.
The next three lines set the configuration information Cargo needs to compile
your program: the name, the version, and the edition of Rust to use. We’ll talk
about the edition
key in Appendix E.
The last line, [dependencies]
, is the start of a section for you to list any
of your project’s dependencies. In Rust, packages of code are referred to as
crates. We won’t need any other crates for this project, but we will in the
first project in Chapter 2, so we’ll use this dependencies section then.
Now open src/main.rs and take a look:
Filename: src/main.rs
fn main() { println!("Hello, world!"); }
Cargo has generated a “Hello, world!” program for you, just like the one we wrote in Listing 1-1! So far, the differences between our project and the project Cargo generated are that Cargo placed the code in the src directory and we have a Cargo.toml configuration file in the top directory.
Cargo expects your source files to live inside the src directory. The top-level project directory is just for README files, license information, configuration files, and anything else not related to your code. Using Cargo helps you organize your projects. There’s a place for everything, and everything is in its place.
If you started a project that doesn’t use Cargo, as we did with the “Hello,
world!” project, you can convert it to a project that does use Cargo. Move the
project code into the src directory and create an appropriate Cargo.toml
file. One easy way to get that Cargo.toml file is to run cargo init
, which
will create it for you automatically.
Building and Running a Cargo Project
Now let’s look at what’s different when we build and run the “Hello, world!” program with Cargo! From your hello_cargo directory, build your project by entering the following command:
$ cargo build
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs
This command creates an executable file in target/debug/hello_cargo (or target\debug\hello_cargo.exe on Windows) rather than in your current directory. Because the default build is a debug build, Cargo puts the binary in a directory named debug. You can run the executable with this command:
$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
Hello, world!
If all goes well, Hello, world!
should print to the terminal. Running cargo build
for the first time also causes Cargo to create a new file at the top
level: Cargo.lock. This file keeps track of the exact versions of
dependencies in your project. This project doesn’t have dependencies, so the
file is a bit sparse. You won’t ever need to change this file manually; Cargo
manages its contents for you.
We just built a project with cargo build
and ran it with
./target/debug/hello_cargo
, but we can also use cargo run
to compile the
code and then run the resultant executable all in one command:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, world!
Using cargo run
is more convenient than having to remember to run cargo build
and then use the whole path to the binary, so most developers use cargo run
.
Notice that this time we didn’t see output indicating that Cargo was compiling
hello_cargo
. Cargo figured out that the files hadn’t changed, so it didn’t
rebuild but just ran the binary. If you had modified your source code, Cargo
would have rebuilt the project before running it, and you would have seen this
output:
$ cargo run
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
Running `target/debug/hello_cargo`
Hello, world!
Cargo also provides a command called cargo check
. This command quickly checks
your code to make sure it compiles but doesn’t produce an executable:
$ cargo check
Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
Why would you not want an executable? Often, cargo check
is much faster than
cargo build
because it skips the step of producing an executable. If you’re
continually checking your work while writing the code, using cargo check
will
speed up the process of letting you know if your project is still compiling! As
such, many Rustaceans run cargo check
periodically as they write their
program to make sure it compiles. Then they run cargo build
when they’re
ready to use the executable.
Let’s recap what we’ve learned so far about Cargo:
- We can create a project using
cargo new
. - We can build a project using
cargo build
. - We can build and run a project in one step using
cargo run
. - We can build a project without producing a binary to check for errors using
cargo check
. - Instead of saving the result of the build in the same directory as our code, Cargo stores it in the target/debug directory.
An additional advantage of using Cargo is that the commands are the same no matter which operating system you’re working on. So, at this point, we’ll no longer provide specific instructions for Linux and macOS versus Windows.
Building for Release
When your project is finally ready for release, you can use cargo build --release
to compile it with optimizations. This command will create an
executable in target/release instead of target/debug. The optimizations
make your Rust code run faster, but turning them on lengthens the time it takes
for your program to compile. This is why there are two different profiles: one
for development, when you want to rebuild quickly and often, and another for
building the final program you’ll give to a user that won’t be rebuilt
repeatedly and that will run as fast as possible. If you’re benchmarking your
code’s running time, be sure to run cargo build --release
and benchmark with
the executable in target/release.
Cargo as Convention
With simple projects, Cargo doesn’t provide a lot of value over just using
rustc
, but it will prove its worth as your programs become more intricate.
Once programs grow to multiple files or need a dependency, it’s much easier to
let Cargo coordinate the build.
Even though the hello_cargo
project is simple, it now uses much of the real
tooling you’ll use in the rest of your Rust career. In fact, to work on any
existing projects, you can use the following commands to check out the code
using Git, change to that project’s directory, and build:
$ git clone example.org/someproject
$ cd someproject
$ cargo build
For more information about Cargo, check out its documentation.
Summary
You’re already off to a great start on your Rust journey! In this chapter, you’ve learned how to:
- Install the latest stable version of Rust using
rustup
- Update to a newer Rust version
- Open locally installed documentation
- Write and run a “Hello, world!” program using
rustc
directly - Create and run a new project using the conventions of Cargo
This is a great time to build a more substantial program to get used to reading and writing Rust code. So, in Chapter 2, we’ll build a guessing game program. If you would rather start by learning how common programming concepts work in Rust, see Chapter 3 and then return to Chapter 2.
Programming a Guessing Game
Let’s jump into Rust by working through a hands-on project together! This
chapter introduces you to a few common Rust concepts by showing you how to use
them in a real program. You’ll learn about let
, match
, methods, associated
functions, external crates, and more! In the following chapters, we’ll explore
these ideas in more detail. In this chapter, you’ll just practice the
fundamentals.
We’ll implement a classic beginner programming problem: a guessing game. Here’s how it works: the program will generate a random integer between 1 and 100. It will then prompt the player to enter a guess. After a guess is entered, the program will indicate whether the guess is too low or too high. If the guess is correct, the game will print a congratulatory message and exit.
Note: there are no quizzes in this chapter, since it is just supposed to give you a feel for the language.
Setting Up a New Project
To set up a new project, go to the projects directory that you created in Chapter 1 and make a new project using Cargo, like so:
$ cargo new guessing_game
$ cd guessing_game
The first command, cargo new
, takes the name of the project (guessing_game
)
as the first argument. The second command changes to the new project’s
directory.
Look at the generated Cargo.toml file:
Filename: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
As you saw in Chapter 1, cargo new
generates a “Hello, world!” program for
you. Check out the src/main.rs file:
Filename: src/main.rs
fn main() { println!("Hello, world!"); }
Now let’s compile this “Hello, world!” program and run it in the same step
using the cargo run
command:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Hello, world!
The run
command comes in handy when you need to rapidly iterate on a project,
as we’ll do in this game, quickly testing each iteration before moving on to
the next one.
Reopen the src/main.rs file. You’ll be writing all the code in this file.
Processing a Guess
The first part of the guessing game program will ask for user input, process that input, and check that the input is in the expected form. To start, we’ll allow the player to input a guess. Enter the code in Listing 2-1 into src/main.rs.
This code contains a lot of information, so let’s go over it line by line. To
obtain user input and then print the result as output, we need to bring the
io
input/output library into scope. The io
library comes from the standard
library, known as std
:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
By default, Rust has a set of items defined in the standard library that it brings into the scope of every program. This set is called the prelude, and you can see everything in it in the standard library documentation.
If a type you want to use isn’t in the prelude, you have to bring that type
into scope explicitly with a use
statement. Using the std::io
library
provides you with a number of useful features, including the ability to accept
user input.
As you saw in Chapter 1, the main
function is the entry point into the
program:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
The fn
syntax declares a new function; the parentheses, ()
, indicate there
are no parameters; and the curly bracket, {
, starts the body of the function.
As you also learned in Chapter 1, println!
is a macro that prints a string to
the screen:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
This code is printing a prompt stating what the game is and requesting input from the user.
Storing Values with Variables
Next, we’ll create a variable to store the user input, like this:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Now the program is getting interesting! There’s a lot going on in this little
line. We use the let
statement to create the variable. Here’s another example:
let apples = 5;
This line creates a new variable named apples
and binds it to the value 5. In
Rust, variables are immutable by default, meaning once we give the variable a
value, the value won’t change. We’ll be discussing this concept in detail in
the “Variables and Mutability”
section in Chapter 3. To make a variable mutable, we add mut
before the
variable name:
let apples = 5; // immutable
let mut bananas = 5; // mutable
Note: The //
syntax starts a comment that continues until the end of the
line. Rust ignores everything in comments. We’ll discuss comments in more
detail in Chapter 3.
Returning to the guessing game program, you now know that let mut guess
will
introduce a mutable variable named guess
. The equal sign (=
) tells Rust we
want to bind something to the variable now. On the right of the equal sign is
the value that guess
is bound to, which is the result of calling
String::new
, a function that returns a new instance of a String
.
String
is a string type provided by the standard
library that is a growable, UTF-8 encoded bit of text.
The ::
syntax in the ::new
line indicates that new
is an associated
function of the String
type. An associated function is a function that’s
implemented on a type, in this case String
. This new
function creates a
new, empty string. You’ll find a new
function on many types because it’s a
common name for a function that makes a new value of some kind.
In full, the let mut guess = String::new();
line has created a mutable
variable that is currently bound to a new, empty instance of a String
. Whew!
Receiving User Input
Recall that we included the input/output functionality from the standard
library with use std::io;
on the first line of the program. Now we’ll call
the stdin
function from the io
module, which will allow us to handle user
input:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
If we hadn’t imported the io
library with use std::io;
at the beginning of
the program, we could still use the function by writing this function call as
std::io::stdin
. The stdin
function returns an instance of
std::io::Stdin
, which is a type that represents a
handle to the standard input for your terminal.
Next, the line .read_line(&mut guess)
calls the read_line
method on the standard input handle to get input from the user.
We’re also passing &mut guess
as the argument to read_line
to tell it what
string to store the user input in. The full job of read_line
is to take
whatever the user types into standard input and append that into a string
(without overwriting its contents), so we therefore pass that string as an
argument. The string argument needs to be mutable so the method can change the
string’s content.
The &
indicates that this argument is a reference, which gives you a way to
let multiple parts of your code access one piece of data without needing to
copy that data into memory multiple times. References are a complex feature,
and one of Rust’s major advantages is how safe and easy it is to use
references. You don’t need to know a lot of those details to finish this
program. For now, all you need to know is that, like variables, references are
immutable by default. Hence, you need to write &mut guess
rather than
&guess
to make it mutable. (Chapter 4 will explain references more
thoroughly.)
Handling Potential Failure with Result
We’re still working on this line of code. We’re now discussing a third line of text, but note that it’s still part of a single logical line of code. The next part is this method:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
We could have written this code as:
io::stdin().read_line(&mut guess).expect("Failed to read line");
However, one long line is difficult to read, so it’s best to divide it. It’s
often wise to introduce a newline and other whitespace to help break up long
lines when you call a method with the .method_name()
syntax. Now let’s
discuss what this line does.
As mentioned earlier, read_line
puts whatever the user enters into the string
we pass to it, but it also returns a Result
value. Result
is an enumeration, often called an enum,
which is a type that can be in one of multiple possible states. We call each
possible state a variant.
Chapter 6 will cover enums in more detail. The purpose
of these Result
types is to encode error-handling information.
Result
’s variants are Ok
and Err
. The Ok
variant indicates the
operation was successful, and inside Ok
is the successfully generated value.
The Err
variant means the operation failed, and Err
contains information
about how or why the operation failed.
Values of the Result
type, like values of any type, have methods defined on
them. An instance of Result
has an expect
method
that you can call. If this instance of Result
is an Err
value, expect
will cause the program to crash and display the message that you passed as an
argument to expect
. If the read_line
method returns an Err
, it would
likely be the result of an error coming from the underlying operating system.
If this instance of Result
is an Ok
value, expect
will take the return
value that Ok
is holding and return just that value to you so you can use it.
In this case, that value is the number of bytes in the user’s input.
If you don’t call expect
, the program will compile, but you’ll get a warning:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
10 | let _ = io::stdin().read_line(&mut guess);
| +++++++
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s
Rust warns that you haven’t used the Result
value returned from read_line
,
indicating that the program hasn’t handled a possible error.
The right way to suppress the warning is to actually write error-handling code,
but in our case we just want to crash this program when a problem occurs, so we
can use expect
. You’ll learn about recovering from errors in Chapter
9.
Printing Values with println!
Placeholders
Aside from the closing curly bracket, there’s only one more line to discuss in the code so far:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
This line prints the string that now contains the user’s input. The {}
set of
curly brackets is a placeholder: think of {}
as little crab pincers that hold
a value in place. When printing the value of a variable, the variable name can
go inside the curly brackets. When printing the result of evaluating an
expression, place empty curly brackets in the format string, then follow the
format string with a comma-separated list of expressions to print in each empty
curly bracket placeholder in the same order. Printing a variable and the result
of an expression in one call to println!
would look like this:
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {x} and y + 2 = {}", y + 2); }
This code would print x = 5 and y + 2 = 12
.
Testing the First Part
Let’s test the first part of the guessing game. Run it using cargo run
:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
At this point, the first part of the game is done: we’re getting input from the keyboard and then printing it.
Generating a Secret Number
Next, we need to generate a secret number that the user will try to guess. The
secret number should be different every time so the game is fun to play more
than once. We’ll use a random number between 1 and 100 so the game isn’t too
difficult. Rust doesn’t yet include random number functionality in its standard
library. However, the Rust team does provide a rand
crate with
said functionality.
Using a Crate to Get More Functionality
Remember that a crate is a collection of Rust source code files. The project
we’ve been building is a binary crate, which is an executable. The rand
crate is a library crate, which contains code that is intended to be used in
other programs and can’t be executed on its own.
Cargo’s coordination of external crates is where Cargo really shines. Before we
can write code that uses rand
, we need to modify the Cargo.toml file to
include the rand
crate as a dependency. Open that file now and add the
following line to the bottom, beneath the [dependencies]
section header that
Cargo created for you. Be sure to specify rand
exactly as we have here, with
this version number, or the code examples in this tutorial may not work:
Filename: Cargo.toml
[dependencies]
rand = "0.8.5"
In the Cargo.toml file, everything that follows a header is part of that
section that continues until another section starts. In [dependencies]
you
tell Cargo which external crates your project depends on and which versions of
those crates you require. In this case, we specify the rand
crate with the
semantic version specifier 0.8.5
. Cargo understands Semantic
Versioning (sometimes called SemVer), which is a
standard for writing version numbers. The specifier 0.8.5
is actually
shorthand for ^0.8.5
, which means any version that is at least 0.8.5 but
below 0.9.0.
Cargo considers these versions to have public APIs compatible with version 0.8.5, and this specification ensures you’ll get the latest patch release that will still compile with the code in this chapter. Any version 0.9.0 or greater is not guaranteed to have the same API as what the following examples use.
Now, without changing any of the code, let’s build the project, as shown in Listing 2-2.
You may see different version numbers (but they will all be compatible with the code, thanks to SemVer!) and different lines (depending on the operating system), and the lines may be in a different order.
When we include an external dependency, Cargo fetches the latest versions of everything that dependency needs from the registry, which is a copy of data from Crates.io. Crates.io is where people in the Rust ecosystem post their open source Rust projects for others to use.
After updating the registry, Cargo checks the [dependencies]
section and
downloads any crates listed that aren’t already downloaded. In this case,
although we only listed rand
as a dependency, Cargo also grabbed other crates
that rand
depends on to work. After downloading the crates, Rust compiles
them and then compiles the project with the dependencies available.
If you immediately run cargo build
again without making any changes, you
won’t get any output aside from the Finished
line. Cargo knows it has already
downloaded and compiled the dependencies, and you haven’t changed anything
about them in your Cargo.toml file. Cargo also knows that you haven’t changed
anything about your code, so it doesn’t recompile that either. With nothing to
do, it simply exits.
If you open the src/main.rs file, make a trivial change, and then save it and build again, you’ll only see two lines of output:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
These lines show that Cargo only updates the build with your tiny change to the src/main.rs file. Your dependencies haven’t changed, so Cargo knows it can reuse what it has already downloaded and compiled for those.
Ensuring Reproducible Builds with the Cargo.lock File
Cargo has a mechanism that ensures you can rebuild the same artifact every time
you or anyone else builds your code: Cargo will use only the versions of the
dependencies you specified until you indicate otherwise. For example, say that
next week version 0.8.6 of the rand
crate comes out, and that version
contains an important bug fix, but it also contains a regression that will
break your code. To handle this, Rust creates the Cargo.lock file the first
time you run cargo build
, so we now have this in the guessing_game
directory.
When you build a project for the first time, Cargo figures out all the versions of the dependencies that fit the criteria and then writes them to the Cargo.lock file. When you build your project in the future, Cargo will see that the Cargo.lock file exists and will use the versions specified there rather than doing all the work of figuring out versions again. This lets you have a reproducible build automatically. In other words, your project will remain at 0.8.5 until you explicitly upgrade, thanks to the Cargo.lock file. Because the Cargo.lock file is important for reproducible builds, it’s often checked into source control with the rest of the code in your project.
Updating a Crate to Get a New Version
When you do want to update a crate, Cargo provides the command update
,
which will ignore the Cargo.lock file and figure out all the latest versions
that fit your specifications in Cargo.toml. Cargo will then write those
versions to the Cargo.lock file. In this case, Cargo will only look for
versions greater than 0.8.5 and less than 0.9.0. If the rand
crate has
released the two new versions 0.8.6 and 0.9.0, you would see the following if
you ran cargo update
:
$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
Cargo ignores the 0.9.0 release. At this point, you would also notice a change
in your Cargo.lock file noting that the version of the rand
crate you are
now using is 0.8.6. To use rand
version 0.9.0 or any version in the 0.9.x
series, you’d have to update the Cargo.toml file to look like this instead:
[dependencies]
rand = "0.9.0"
The next time you run cargo build
, Cargo will update the registry of crates
available and reevaluate your rand
requirements according to the new version
you have specified.
There’s a lot more to say about Cargo and its ecosystem, which we’ll discuss in Chapter 14, but for now, that’s all you need to know. Cargo makes it very easy to reuse libraries, so Rustaceans are able to write smaller projects that are assembled from a number of packages.
Generating a Random Number
Let’s start using rand
to generate a number to guess. The next step is to
update src/main.rs, as shown in Listing 2-3.
First we add the line use rand::Rng;
. The Rng
trait defines methods that
random number generators implement, and this trait must be in scope for us to
use those methods. Chapter 10 will cover traits in detail.
Next, we’re adding two lines in the middle. In the first line, we call the
rand::thread_rng
function that gives us the particular random number
generator we’re going to use: one that is local to the current thread of
execution and is seeded by the operating system. Then we call the gen_range
method on the random number generator. This method is defined by the Rng
trait that we brought into scope with the use rand::Rng;
statement. The
gen_range
method takes a range expression as an argument and generates a
random number in the range. The kind of range expression we’re using here takes
the form start..=end
and is inclusive on the lower and upper bounds, so we
need to specify 1..=100
to request a number between 1 and 100.
Note: You won’t just know which traits to use and which methods and functions
to call from a crate, so each crate has documentation with instructions for
using it. Another neat feature of Cargo is that running the cargo doc --open
command will build documentation provided by all your dependencies
locally and open it in your browser. If you’re interested in other
functionality in the rand
crate, for example, run cargo doc --open
and
click rand
in the sidebar on the left.
The second new line prints the secret number. This is useful while we’re developing the program to be able to test it, but we’ll delete it from the final version. It’s not much of a game if the program prints the answer as soon as it starts!
Try running the program a few times:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
You should get different random numbers, and they should all be numbers between 1 and 100. Great job!
Comparing the Guess to the Secret Number
Now that we have user input and a random number, we can compare them. That step is shown in Listing 2-4. Note that this code won’t compile just yet, as we will explain.
First we add another use
statement, bringing a type called
std::cmp::Ordering
into scope from the standard library. The Ordering
type
is another enum and has the variants Less
, Greater
, and Equal
. These are
the three outcomes that are possible when you compare two values.
Then we add five new lines at the bottom that use the Ordering
type. The
cmp
method compares two values and can be called on anything that can be
compared. It takes a reference to whatever you want to compare with: here it’s
comparing guess
to secret_number
. Then it returns a variant of the
Ordering
enum we brought into scope with the use
statement. We use a
match
expression to decide what to do next based on
which variant of Ordering
was returned from the call to cmp
with the values
in guess
and secret_number
.
A match
expression is made up of arms. An arm consists of a pattern to
match against, and the code that should be run if the value given to match
fits that arm’s pattern. Rust takes the value given to match
and looks
through each arm’s pattern in turn. Patterns and the match
construct are
powerful Rust features: they let you express a variety of situations your code
might encounter and they make sure you handle them all. These features will be
covered in detail in Chapter 6 and Chapter 18, respectively.
Let’s walk through an example with the match
expression we use here. Say that
the user has guessed 50 and the randomly generated secret number this time is
38.
When the code compares 50 to 38, the cmp
method will return
Ordering::Greater
because 50 is greater than 38. The match
expression gets
the Ordering::Greater
value and starts checking each arm’s pattern. It looks
at the first arm’s pattern, Ordering::Less
, and sees that the value
Ordering::Greater
does not match Ordering::Less
, so it ignores the code in
that arm and moves to the next arm. The next arm’s pattern is
Ordering::Greater
, which does match Ordering::Greater
! The associated
code in that arm will execute and print Too big!
to the screen. The match
expression ends after the first successful match, so it won’t look at the last
arm in this scenario.
However, the code in Listing 2-4 won’t compile yet. Let’s try it:
$ cargo build
Downloading crates ...
Downloaded rand_core v0.6.2
Downloaded getrandom v0.2.2
Downloaded rand_chacha v0.3.0
Downloaded ppv-lite86 v0.2.10
Downloaded libc v0.2.86
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
| |
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
--> /rustc/eeb90cda1969383f56a2637cbd3037bdf598841c/library/core/src/cmp.rs:839:8
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error
The core of the error states that there are mismatched types. Rust has a
strong, static type system. However, it also has type inference. When we wrote
let mut guess = String::new()
, Rust was able to infer that guess
should be
a String
and didn’t make us write the type. The secret_number
, on the other
hand, is a number type. A few of Rust’s number types can have a value between 1
and 100: i32
, a 32-bit number; u32
, an unsigned 32-bit number; i64
, a
64-bit number; as well as others. Unless otherwise specified, Rust defaults to
an i32
, which is the type of secret_number
unless you add type information
elsewhere that would cause Rust to infer a different numerical type. The reason
for the error is that Rust cannot compare a string and a number type.
Ultimately, we want to convert the String
the program reads as input into a
number type so we can compare it numerically to the secret number. We do so by
adding this line to the main
function body:
Filename: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
The line is:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
We create a variable named guess
. But wait, doesn’t the program already have
a variable named guess
? It does, but helpfully Rust allows us to shadow the
previous value of guess
with a new one. Shadowing lets us reuse the guess
variable name rather than forcing us to create two unique variables, such as
guess_str
and guess
, for example. We’ll cover this in more detail in
Chapter 3, but for now, know that this feature is
often used when you want to convert a value from one type to another type.
We bind this new variable to the expression guess.trim().parse()
. The guess
in the expression refers to the original guess
variable that contained the
input as a string. The trim
method on a String
instance will eliminate any
whitespace at the beginning and end, which we must do to be able to compare the
string to the u32
, which can only contain numerical data. The user must press
enter to satisfy read_line
and input their guess, which adds a
newline character to the string. For example, if the user types 5 and
presses enter, guess
looks like this: 5\n
. The \n
represents
“newline.” (On Windows, pressing enter results in a carriage return
and a newline, \r\n
.) The trim
method eliminates \n
or \r\n
, resulting
in just 5
.
The parse
method on strings converts a string to
another type. Here, we use it to convert from a string to a number. We need to
tell Rust the exact number type we want by using let guess: u32
. The colon
(:
) after guess
tells Rust we’ll annotate the variable’s type. Rust has a
few built-in number types; the u32
seen here is an unsigned, 32-bit integer.
It’s a good default choice for a small positive number. You’ll learn about
other number types in Chapter 3.
Additionally, the u32
annotation in this example program and the comparison
with secret_number
means Rust will infer that secret_number
should be a
u32
as well. So now the comparison will be between two values of the same
type!
The parse
method will only work on characters that can logically be converted
into numbers and so can easily cause errors. If, for example, the string
contained A👍%
, there would be no way to convert that to a number. Because it
might fail, the parse
method returns a Result
type, much as the read_line
method does (discussed earlier in “Handling Potential Failure with
Result
”). We’ll treat
this Result
the same way by using the expect
method again. If parse
returns an Err
Result
variant because it couldn’t create a number from the
string, the expect
call will crash the game and print the message we give it.
If parse
can successfully convert the string to a number, it will return the
Ok
variant of Result
, and expect
will return the number that we want from
the Ok
value.
Let’s run the program now:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
Nice! Even though spaces were added before the guess, the program still figured out that the user guessed 76. Run the program a few times to verify the different behavior with different kinds of input: guess the number correctly, guess a number that is too high, and guess a number that is too low.
We have most of the game working now, but the user can make only one guess. Let’s change that by adding a loop!
Allowing Multiple Guesses with Looping
The loop
keyword creates an infinite loop. We’ll add a loop to give users
more chances at guessing the number:
Filename: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
As you can see, we’ve moved everything from the guess input prompt onward into a loop. Be sure to indent the lines inside the loop another four spaces each and run the program again. The program will now ask for another guess forever, which actually introduces a new problem. It doesn’t seem like the user can quit!
The user could always interrupt the program by using the keyboard shortcut
ctrl-c. But there’s another way to escape this insatiable
monster, as mentioned in the parse
discussion in “Comparing the Guess to the
Secret Number”: if
the user enters a non-number answer, the program will crash. We can take
advantage of that to allow the user to quit, as shown here:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Typing quit
will quit the game, but as you’ll notice, so will entering any
other non-number input. This is suboptimal, to say the least; we want the game
to also stop when the correct number is guessed.
Quitting After a Correct Guess
Let’s program the game to quit when the user wins by adding a break
statement:
Filename: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Adding the break
line after You win!
makes the program exit the loop when
the user guesses the secret number correctly. Exiting the loop also means
exiting the program, because the loop is the last part of main
.
Handling Invalid Input
To further refine the game’s behavior, rather than crashing the program when
the user inputs a non-number, let’s make the game ignore a non-number so the
user can continue guessing. We can do that by altering the line where guess
is converted from a String
to a u32
, as shown in Listing 2-5.
We switch from an expect
call to a match
expression to move from crashing
on an error to handling the error. Remember that parse
returns a Result
type and Result
is an enum that has the variants Ok
and Err
. We’re using
a match
expression here, as we did with the Ordering
result of the cmp
method.
If parse
is able to successfully turn the string into a number, it will
return an Ok
value that contains the resultant number. That Ok
value will
match the first arm’s pattern, and the match
expression will just return the
num
value that parse
produced and put inside the Ok
value. That number
will end up right where we want it in the new guess
variable we’re creating.
If parse
is not able to turn the string into a number, it will return an
Err
value that contains more information about the error. The Err
value
does not match the Ok(num)
pattern in the first match
arm, but it does
match the Err(_)
pattern in the second arm. The underscore, _
, is a
catchall value; in this example, we’re saying we want to match all Err
values, no matter what information they have inside them. So the program will
execute the second arm’s code, continue
, which tells the program to go to the
next iteration of the loop
and ask for another guess. So, effectively, the
program ignores all errors that parse
might encounter!
Now everything in the program should work as expected. Let’s try it:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 4.45s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
Awesome! With one tiny final tweak, we will finish the guessing game. Recall
that the program is still printing the secret number. That worked well for
testing, but it ruins the game. Let’s delete the println!
that outputs the
secret number. Listing 2-6 shows the final code.
At this point, you’ve successfully built the guessing game. Congratulations!
Summary
This project was a hands-on way to introduce you to many new Rust concepts:
let
, match
, functions, the use of external crates, and more. In the next
few chapters, you’ll learn about these concepts in more detail. Chapter 3
covers concepts that most programming languages have, such as variables, data
types, and functions, and shows how to use them in Rust. Chapter 4 explores
ownership, a feature that makes Rust different from other languages. Chapter 5
discusses structs and method syntax, and Chapter 6 explains how enums work.
Common Programming Concepts
This chapter covers concepts that appear in almost every programming language and how they work in Rust. Many programming languages have much in common at their core. None of the concepts presented in this chapter are unique to Rust, but we’ll discuss them in the context of Rust and explain the conventions around using these concepts.
Specifically, you’ll learn about variables, basic types, functions, comments, and control flow. These foundations will be in every Rust program, and learning them early will give you a strong core to start from.
Keywords
The Rust language has a set of keywords that are reserved for use by the language only, much as in other languages. Keep in mind that you cannot use these words as names of variables or functions. Most of the keywords have special meanings, and you’ll be using them to do various tasks in your Rust programs; a few have no current functionality associated with them but have been reserved for functionality that might be added to Rust in the future. You can find a list of the keywords in Appendix A.
Variables and Mutability
As mentioned in the “Storing Values with Variables” section, by default, variables are immutable. This is one of many nudges Rust gives you to write your code in a way that takes advantage of the safety and easy concurrency that Rust offers. However, you still have the option to make your variables mutable. Let’s explore how and why Rust encourages you to favor immutability and why sometimes you might want to opt out.
When a variable is immutable, once a value is bound to a name, you can’t change
that value. To illustrate this, generate a new project called variables in
your projects directory by using cargo new variables
.
Then, in your new variables directory, open src/main.rs and replace its code with the following code, which won’t compile just yet:
Filename: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Save and run the program using cargo run
. You should receive an error message
regarding an immutability error, as shown in this output:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
This example shows how the compiler helps you find errors in your programs. Compiler errors can be frustrating, but really they only mean your program isn’t safely doing what you want it to do yet; they do not mean that you’re not a good programmer! Experienced Rustaceans still get compiler errors.
You received the error message cannot assign twice to immutable variable `x`
because you tried to assign a second value to the immutable x
variable.
It’s important that we get compile-time errors when we attempt to change a value that’s designated as immutable because this very situation can lead to bugs. If one part of our code operates on the assumption that a value will never change and another part of our code changes that value, it’s possible that the first part of the code won’t do what it was designed to do. The cause of this kind of bug can be difficult to track down after the fact, especially when the second piece of code changes the value only sometimes. The Rust compiler guarantees that when you state that a value won’t change, it really won’t change, so you don’t have to keep track of it yourself. Your code is thus easier to reason through.
But mutability can be very useful, and can make code more convenient to write.
Although variables are immutable by default, you can make them mutable by
adding mut
in front of the variable name as you did in Chapter
2. Adding mut
also conveys
intent to future readers of the code by indicating that other parts of the code
will be changing this variable’s value.
For example, let’s change src/main.rs to the following:
Filename: src/main.rs
fn main() { let mut x = 5; println!("The value of x is: {x}"); x = 6; println!("The value of x is: {x}"); }
When we run the program now, we get this:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
We’re allowed to change the value bound to x
from 5
to 6
when mut
is
used. Ultimately, deciding whether to use mutability or not is up to you and
depends on what you think is clearest in that particular situation.
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.
First, you aren’t allowed to use mut
with constants. Constants aren’t just
immutable by default—they’re always immutable. You declare constants using the
const
keyword instead of the let
keyword, and the type of the value must
be annotated. We’ll cover types and type annotations in the next section,
“Data Types”, so don’t worry about the details
right now. Just know that you must always annotate the type.
Constants can be declared in any scope, including the global scope, which makes them useful for values that many parts of code need to know about.
The last difference is that constants may be set only to a constant expression, not the result of a value that could only be computed at runtime.
Here’s an example of a constant declaration:
#![allow(unused)] fn main() { const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; }
The constant’s name is THREE_HOURS_IN_SECONDS
and its value is set to the
result of multiplying 60 (the number of seconds in a minute) by 60 (the number
of minutes in an hour) by 3 (the number of hours we want to count in this
program). Rust’s naming convention for constants is to use all uppercase with
underscores between words. The compiler is able to evaluate a limited set of
operations at compile time, which lets us choose to write out this value in a
way that’s easier to understand and verify, rather than setting this constant
to the value 10,800. See the Rust Reference’s section on constant
evaluation for more information on what operations can be used
when declaring constants.
Constants are valid for the entire time a program runs, within the scope in which they were declared. This property makes constants useful for values in your application domain that multiple parts of the program might need to know about, such as the maximum number of points any player of a game is allowed to earn, or the speed of light.
Naming hardcoded values used throughout your program as constants is useful in conveying the meaning of that value to future maintainers of the code. It also helps to have only one place in your code you would need to change if the hardcoded value needed to be updated in the future.
Shadowing
As you saw in the guessing game tutorial in Chapter
2, you can declare a
new variable with the same name as a previous variable. Rustaceans say that the
first variable is shadowed by the second, which means that the second
variable is what the compiler will see when you use the name of the variable.
In effect, 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.
We can shadow a variable by using the same variable’s name and repeating the
use of the let
keyword as follows:
Filename: src/main.rs
fn main() { let x = 5; let x = x + 1; { let x = x * 2; println!("The value of x in the inner scope is: {x}"); } println!("The value of x is: {x}"); }
This program first binds x
to a value of 5
. Then it creates a new variable
x
by repeating let x =
, taking the original value and adding 1
so the
value of x
is then 6
. Then, within an inner scope created with the curly
brackets, the third let
statement also shadows x
and creates a new
variable, multiplying the previous value by 2
to give x
a value of 12
.
When that scope is over, the inner shadowing ends and x
returns to being 6
.
When we run this program, it will output the following:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
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. For example, say our
program asks a user to show how many spaces they want between some text by
inputting space characters, and then we want to store that input as a number:
fn main() { let spaces = " "; let spaces = spaces.len(); }
The first spaces
variable is a string type and the second spaces
variable
is a number type. Shadowing thus spares us from having to come up with
different names, such as spaces_str
and spaces_num
; instead, we can reuse
the simpler spaces
name. However, if we try to use mut
for this, as shown
here, we’ll get a compile-time error:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
The error says we’re not allowed to mutate a variable’s type:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
Now that we’ve explored how variables work, let’s look at more data types they can have.
Data Types
Every value in Rust is of a certain data type, which tells Rust what kind of data is being specified so it knows how to work with that data. We’ll look at two data type subsets: scalar and compound.
Keep in mind that Rust is a statically typed language, which means that it
must know the types of all variables at compile time. The compiler can usually
infer what type we want to use based on the value and how we use it. In cases
when many types are possible, such as when we converted a String
to a numeric
type using parse
in the “Comparing the Guess to the Secret
Number” section in
Chapter 2, we must add a type annotation, like this:
#![allow(unused)] fn main() { let guess: u32 = "42".parse().expect("Not a number!"); }
If we don’t add the : u32
type annotation shown in the preceding code, Rust
will display the following error, which means the compiler needs more
information from us to know which type we want to use:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++
For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
You’ll see different type annotations for other data types.
Scalar Types
A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters. You may recognize these from other programming languages. Let’s jump into how they work in Rust.
Integer Types
An integer is a number without a fractional component. We used one integer
type in Chapter 2, the u32
type. This type declaration indicates that the
value it’s associated with should be an unsigned integer (signed integer types
start with i
instead of u
) that takes up 32 bits of space. Table 3-1 shows
the built-in integer types in Rust. We can use any of these variants to declare
the type of an integer value.
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 variant can be either signed or unsigned and has an explicit size. 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). It’s like writing numbers on paper: when the sign matters, a number is shown with a plus sign or a minus sign; however, when it’s safe to assume the number is positive, it’s shown with no sign. Signed numbers are stored using two’s complement representation.
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.
Additionally, the isize
and usize
types depend on the architecture of the
computer your program is running on, which is denoted in the table as “arch”:
64 bits if you’re on a 64-bit architecture and 32 bits if you’re on a 32-bit
architecture.
You can write integer literals in any of the forms shown in Table 3-2. Note
that number literals that can be multiple numeric types allow a type suffix,
such as 57u8
, to designate the type. Number literals can also use _
as a
visual separator to make the number easier to read, such as 1_000
, which will
have the same value as if you had specified 1000
.
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
.
The primary situation in which you’d use isize
or usize
is when indexing
some sort of collection.
Integer Overflow
Let’s say you have a variable of type u8
that can hold values between 0 and
255. If you try to change the variable to a value outside that range, such as
256, integer overflow will occur, which can result in one of two behaviors.
When you’re compiling in debug mode, Rust includes checks for integer overflow
that cause your program to panic at runtime if this behavior occurs. Rust
uses the term panicking when a program exits with an error; we’ll discuss
panics in more depth in the “Unrecoverable Errors with
panic!
” section in Chapter
9.
When you’re compiling in release mode with the --release
flag, Rust does
not include checks for integer overflow that cause panics. Instead, if
overflow occurs, Rust performs two’s complement wrapping. In short, values
greater than the maximum value the type can hold “wrap around” to the minimum
of the values the type can hold. In the case of a u8
, the value 256 becomes
0, the value 257 becomes 1, and so on. The program won’t panic, but the
variable will have a value that probably isn’t what you were expecting it to
have. Relying on integer overflow’s wrapping behavior is considered an error.
To explicitly handle the possibility of overflow, you can use these families of methods provided by the standard library for primitive numeric types:
- Wrap in all modes with the
wrapping_*
methods, such aswrapping_add
. - Return the
None
value if there is overflow with thechecked_*
methods. - Return the value and a boolean indicating whether there was overflow with
the
overflowing_*
methods. - Saturate at the value’s minimum or maximum values with the
saturating_*
methods.
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.
Here’s an example that shows floating-point numbers in action:
Filename: src/main.rs
fn main() { let x = 2.0; // f64 let y: f32 = 3.0; // f32 }
Floating-point numbers are represented according to the IEEE-754 standard. The
f32
type is a single-precision float, and f64
has double precision.
Numeric Operations
Rust supports the basic mathematical operations you’d expect for all the number
types: addition, subtraction, multiplication, division, and remainder. Integer
division truncates toward zero to the nearest integer. The following code shows
how you’d use each numeric operation in a let
statement:
Filename: src/main.rs
fn main() { // addition let sum = 5 + 10; // subtraction let difference = 95.5 - 4.3; // multiplication let product = 4 * 30; // division let quotient = 56.7 / 32.2; let truncated = -5 / 3; // Results in -1 // remainder let remainder = 43 % 5; }
Each expression in these statements uses a mathematical operator and evaluates to a single value, which is then bound to a variable. Appendix B contains a list of all operators that Rust provides.
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. The Boolean type in
Rust is specified using bool
. For example:
Filename: src/main.rs
fn main() { let t = true; let f: bool = false; // with explicit type annotation }
The main way to use Boolean values is through conditionals, such as an if
expression. We’ll cover how if
expressions work in Rust in the “Control
Flow” section.
The Character Type
Rust’s char
type is the language’s most primitive alphabetic type. Here are
some examples of declaring char
values:
Filename: src/main.rs
fn main() { let c = 'z'; let z: char = 'ℤ'; // with explicit type annotation let heart_eyed_cat = '😻'; }
Note that we specify char
literals with single quotes, as opposed to string
literals, which use double quotes. Rust’s char
type is four bytes in size and
represents a Unicode Scalar Value, which means it can represent a lot more than
just ASCII. Accented letters; Chinese, Japanese, and Korean characters; emoji;
and zero-width spaces are all valid char
values in Rust. Unicode Scalar
Values range from U+0000
to U+D7FF
and U+E000
to U+10FFFF
inclusive.
However, a “character” isn’t really a concept in Unicode, so your human
intuition for what a “character” is may not match up with what a char
is in
Rust. We’ll discuss this topic in detail in “Storing UTF-8 Encoded Text with
Strings” in Chapter 8.
Compound 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. Each position in the tuple has a type, and the types of the different values in the tuple don’t have to be the same. We’ve added optional type annotations in this example:
Filename: src/main.rs
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
The variable tup
binds to the entire tuple because a tuple is considered a
single compound element. To get the individual values out of a tuple, we can
use pattern matching to destructure a tuple value, like this:
Filename: src/main.rs
fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {y}"); }
This program first creates a tuple and binds it to the variable tup
. It then
uses a pattern with let
to take tup
and turn it into three separate
variables, x
, y
, and z
. This is called destructuring because it breaks
the single tuple into three parts. Finally, the program prints the value of
y
, which is 6.4
.
We can also access a tuple element directly by using a period (.
) followed by
the index of the value we want to access. For example:
Filename: src/main.rs
fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; let six_point_four = x.1; let one = x.2; }
This program creates the tuple x
and then accesses each element of the tuple
using their respective indices. As with most programming languages, the first
index in a tuple is 0.
The tuple without any values has a special name, unit. This value and its
corresponding type are both written ()
and represent an empty value or an
empty return type. Expressions implicitly return the unit value if they don’t
return any other value.
Additionally, we can modify individual elements of a mutable tuple. For example:
Filename: src/main.rs
fn main() { let mut x: (i32, i32) = (1, 2); x.0 = 0; x.1 += 5; }
This program sets the first element to zero and adds five to the second element.
The final value of x
is (0, 7)
.
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.
We write the values in an array as a comma-separated list inside square brackets:
Filename: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; }
Arrays are useful when you want your data allocated on the stack rather than the heap (we will discuss the stack and the heap more in Chapter 4) 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. Chapter 8 discusses vectors in more detail.
However, arrays are more useful when you know the number of elements will not need to change. For example, if you were using the names of the month in a program, you would probably use an array rather than a vector because you know it will always contain 12 elements:
#![allow(unused)] fn main() { let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; }
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, like so:
#![allow(unused)] fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; }
Here, i32
is the type of each element. After the semicolon, the number 5
indicates the array contains five elements.
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, as shown here:
#![allow(unused)] fn main() { let a = [3; 5]; }
The array named a
will contain 5
elements that will all be set to the value
3
initially. This is the same as writing let a = [3, 3, 3, 3, 3];
but in a
more concise way.
Accessing Array Elements
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, like this:
Filename: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; }
In this example, the variable named first
will get the value 1
because that
is the value at index [0]
in the array. The variable named second
will get
the value 2
from index [1]
in the array.
Invalid Array Element Access
Let’s see what happens if you try to access an element of an array that is past the end of the array. Say you run this code, similar to the guessing game in Chapter 2, to get an array index from the user:
Filename: src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
This code compiles successfully. If you run this code using cargo run
and
enter 0
, 1
, 2
, 3
, or 4
, the program will print out the corresponding
value at that index in the array. If you instead enter a number past the end of
the array, such as 10
, you’ll see output like this:
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The program resulted in a runtime error at the point of using an invalid
value in the indexing operation. The program exited with an error message and
didn’t execute the final println!
statement. When you attempt to access an
element using indexing, Rust will check that the index you’ve specified is less
than the array length. If the index is greater than or equal to the length,
Rust will panic. This check has to happen at runtime, especially in this case,
because the compiler can’t possibly know what value a user will enter when they
run the code later.
This is an example of Rust’s memory safety principles in action. In many low-level languages, this kind of check is not done, and when you provide an incorrect index, invalid memory can be accessed. Rust protects you against this kind of error by immediately exiting instead of allowing the memory access and continuing. Chapter 9 discusses more of Rust’s error handling and how you can write readable, safe code that neither panics nor allows invalid memory access.
Functions
Functions are prevalent in Rust code. You’ve already seen one of the most
important functions in the language: the main
function, which is the entry
point of many programs. You’ve also seen the fn
keyword, which allows you 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. Here’s a program that contains an example function definition:
Filename: src/main.rs
fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); }
We define a function in Rust by entering fn
followed by a function name and a
set of parentheses. The curly brackets tell the compiler where the function
body begins and ends.
We can call any function we’ve defined by entering its name followed by a set
of parentheses. Because another_function
is defined in the program, it can be
called from inside the main
function. Note that we defined another_function
after the main
function in the source code; we could have defined it before
as well. Rust doesn’t care where you define your functions, only that they’re
defined somewhere in a scope that can be seen by the caller.
Let’s start a new binary project named functions to explore functions
further. Place the another_function
example in src/main.rs and run it. You
should see the following output:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
The lines execute in the order in which they appear in the main
function.
First the “Hello, world!” message prints, and then another_function
is called
and its message is printed.
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.
In this version of another_function
we add a parameter:
Filename: src/main.rs
fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {x}"); }
Try running this program; you should get the following output:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
The declaration of another_function
has one parameter named x
. The type of
x
is specified as i32
. When we pass 5
in to another_function
, the
println!
macro puts 5
where the pair of curly brackets containing x
was
in the format string.
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. The compiler is also able to give more helpful error messages if it knows what types the function expects.
When defining multiple parameters, separate the parameter declarations with commas, like this:
Filename: src/main.rs
fn main() { print_labeled_measurement(5, 'h'); } fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); }
This example creates a function named print_labeled_measurement
with two
parameters. The first parameter is named value
and is an i32
. The second is
named unit_label
and is type char
. The function then prints text containing
both the value
and the unit_label
.
Let’s try running this code. Replace the program currently in your functions
project’s src/main.rs file with the preceding example and run it using cargo run
:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
Because we called the function with 5
as the value for value
and 'h'
as
the value for unit_label
, the program output contains those values.
Statements 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.
Let’s look at some examples.
We’ve actually already used statements and expressions. Creating a variable and
assigning a value to it with the let
keyword is a statement. In Listing 3-1,
let y = 6;
is a statement.
Function definitions are also statements; the entire preceding example is a statement in itself. (As we will see below, calling a function is not a statement.)
Statements do not return values. Therefore, you can’t assign a let
statement
to another variable, as the following code tries to do; you’ll get an error:
Filename: src/main.rs
fn main() {
let x = (let y = 6);
}
When you run this program, the error you’ll get looks like this:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
The let y = 6
statement does not return a value, so there isn’t anything for
x
to bind to. This is different from what happens in other languages, such as
C and Ruby, where the assignment returns the value of the assignment. In those
languages, you can write x = y = 6
and have both x
and y
have the value
6
; that is not the case in Rust.
Expressions evaluate to a value and make up most of the rest of the code that
you’ll write in Rust. Consider a math operation, such as 5 + 6
, which is an
expression that evaluates to the value 11
. Expressions can be part of
statements: in Listing 3-1, the 6
in the statement let y = 6;
is an
expression that evaluates to the value 6
. Calling a function is an
expression. Calling a macro is an expression. A new scope block created with
curly brackets is an expression, for example:
Filename: src/main.rs
fn main() { let y = { let x = 3; x + 1 }; println!("The value of y is: {y}"); }
This expression:
{
let x = 3;
x + 1
}
is a block that, in this case, evaluates to 4
. That value gets bound to y
as part of the let
statement. Note that the x + 1
line doesn’t have a
semicolon at the end, which is unlike most of the lines you’ve seen so far.
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. Here’s an example of a
function that returns a value:
Filename: src/main.rs
fn five() -> i32 { 5 } fn main() { let x = five(); println!("The value of x is: {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. Note that the function’s return type is specified too, as -> i32
. Try
running this code; the output should look like this:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5
The 5
in five
is the function’s return value, which is why the return type
is i32
. Let’s examine this in more detail. There are two important bits:
first, the line let x = five();
shows that we’re using the return value of a
function to initialize a variable. Because the function five
returns a 5
,
that line is the same as the following:
#![allow(unused)] fn main() { let x = 5; }
Second, the five
function has no parameters and defines the type of the
return value. The body of the function is a lonely 5
with no semicolon
because it’s an expression whose value we want to return.
Let’s look at another example:
Filename: src/main.rs
fn main() { let x = plus_one(5); println!("The value of x is: {x}"); } fn plus_one(x: i32) -> i32 { x + 1 }
Running this code will print The value of x is: 6
. But if we place a
semicolon at the end of the line containing x + 1
, changing it from an
expression to a statement, we’ll get an error:
Filename: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
Compiling this code produces an error, as follows:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
The main error message, mismatched types
, reveals the core issue with this
code. The definition of the function plus_one
says that it will return an
i32
, but statements don’t evaluate to a value, which is expressed by ()
,
the unit type. Therefore, nothing is returned, which contradicts the function
definition and results in an error. In this output, Rust provides a message to
possibly help rectify this issue: it suggests removing the semicolon, which
would fix the error.
Comments
All programmers strive to make their code easy to understand, but sometimes extra explanation is warranted. In these cases, programmers leave comments in their source code that the compiler will ignore but people reading the source code may find useful.
Here’s a simple comment:
#![allow(unused)] fn main() { // hello, world }
In Rust, the idiomatic comment style starts a comment with two slashes, and the
comment continues until the end of the line. For comments that extend beyond a
single line, you’ll need to include //
on each line, like this:
#![allow(unused)] fn main() { // So we’re doing something complicated here, long enough that we need // multiple lines of comments to do it! Whew! Hopefully, this comment will // explain what’s going on. }
Or you can use the multiline comment syntax with /*
and */
:
#![allow(unused)] fn main() { /* So we’re doing something complicated here, long enough that we need multiple lines of comments to do it! Whew! Hopefully, this comment will explain what’s going on. */ }
Comments can also be placed at the end of lines containing code:
Filename: src/main.rs
fn main() { let lucky_number = 7; // I’m feeling lucky today }
But you’ll more often see them used in this format, with the comment on a separate line above the code it’s annotating:
Filename: src/main.rs
fn main() { // I’m feeling lucky today let lucky_number = 7; }
Rust also has another kind of comment, documentation comments, which we’ll discuss in the “Publishing a Crate to Crates.io” section of Chapter 14.
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. You
provide a condition and then state, “If this condition is met, run this block
of code. If the condition is not met, do not run this block of code.”
Create a new project called branches in your projects directory to explore
the if
expression. In the src/main.rs file, input the following:
Filename: src/main.rs
fn main() { let number = 3; if number < 5 { println!("condition was true"); } else { println!("condition was false"); } }
All if
expressions start with the keyword if
, followed by a condition. In
this case, the condition checks whether or not the variable number
has a
value less than 5. We place the block of code to execute if the condition is
true
immediately after the condition inside curly brackets. Blocks of code
associated with the conditions in if
expressions are sometimes called arms,
just like the arms in match
expressions that we discussed in the “Comparing
the Guess to the Secret Number” section of Chapter 2.
Optionally, we can also include an else
expression, which we chose to do
here, to give the program an alternative block of code to execute should the
condition evaluate to false
. If you don’t provide an else
expression and
the condition is false
, the program will just skip the if
block and move on
to the next bit of code.
Try running this code; you should see the following output:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
Let’s try changing the value of number
to a value that makes the condition
false
to see what happens:
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
Run the program again, and look at the output:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false
It’s also worth noting that the condition in this code must be a bool
. If
the condition isn’t a bool
, we’ll get an error. For example, try running the
following code:
Filename: src/main.rs
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
The if
condition evaluates to a value of 3
this time, and Rust throws an
error:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
The error indicates that Rust expected a bool
but got an integer. Unlike
languages such as Ruby and JavaScript, Rust will not automatically try to
convert non-Boolean types to a Boolean. You must be explicit and always provide
if
with a Boolean as its condition. If we want the if
code block to run
only when a number is not equal to 0
, for example, we can change the if
expression to the following:
Filename: src/main.rs
fn main() { let number = 3; if number != 0 { println!("number was something other than zero"); } }
Running this code will print number was something other than zero
.
Handling Multiple Conditions with else if
You can use multiple conditions by combining if
and else
in an else if
expression. For example:
Filename: src/main.rs
fn main() { let number = 6; if number % 4 == 0 { println!("number is divisible by 4"); } else if number % 3 == 0 { println!("number is divisible by 3"); } else if number % 2 == 0 { println!("number is divisible by 2"); } else { println!("number is not divisible by 4, 3, or 2"); } }
This program has four possible paths it can take. After running it, you should see the following output:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
When this program executes, it checks each if
expression in turn and executes
the first body for which the condition evaluates to true
. Note that even
though 6 is divisible by 2, we don’t see the output number is divisible by 2
,
nor do we see the number is not divisible by 4, 3, or 2
text from the else
block. That’s because 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 your code, so if you have more
than one, you might want to refactor your code. Chapter 6 describes a powerful
Rust branching construct called match
for these cases.
Using if
in a let
Statement
Because if
is an expression, we can use it on the right side of a let
statement to assign the outcome to a variable, as in Listing 3-2.
The number
variable will be bound to a value based on the outcome of the if
expression. Run this code to see what happens:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
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; in Listing 3-2, the results of both the if
arm and the else
arm were i32
integers. If the types are mismatched, as in the following
example, we’ll get an error:
Filename: src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
When we try to compile this code, we’ll get an error. The if
and else
arms
have value types that are incompatible, and Rust indicates exactly where to
find the problem in the program:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
The expression in the if
block evaluates to an integer, and the expression in
the else
block evaluates to a string. This won’t work because variables must
have a single type, and Rust needs to know at compile time what type the
number
variable is, definitively. Knowing the type of number
lets the
compiler verify the type is valid everywhere we use number
. Rust wouldn’t be
able to do that if the type of number
was only determined at runtime; the
compiler would be more complex and would make fewer guarantees about the code
if it had to keep track of multiple hypothetical types for any variable.
Repetition 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. To experiment with loops, let’s make a new project called loops.
Rust has three kinds of loops: loop
, while
, and for
. Let’s try each one.
Repeating Code with loop
The loop
keyword tells Rust to execute a block of code over and over again
forever or until you explicitly tell it to stop.
As an example, change the src/main.rs file in your loops directory to look like this:
Filename: src/main.rs
fn main() {
loop {
println!("again!");
}
}
When we run this program, we’ll see again!
printed over and over continuously
until we stop the program manually. Most terminals support the keyboard shortcut
ctrl-c to interrupt a program that is stuck in a continual
loop. Give it a try:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
The symbol ^C
represents where you pressed ctrl-c. You
may or may not see the word again!
printed after the ^C
, depending on where
the code was in the loop when it received the interrupt signal.
Fortunately, 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. Recall that we did this in the guessing game in the
“Quitting After a Correct Guess” section of Chapter 2 to exit the program when the user won the game by
guessing the correct number.
We also used continue
in the guessing game, 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.
Returning Values from Loops
One of the uses of a loop
is to retry an operation you know might fail, such
as checking whether a thread has completed its job. You might also need to pass
the result of that operation out of the loop to the rest of your code. To do
this, you can add the value you want returned after the break
expression you
use to stop the loop; that value will be returned out of the loop so you can
use it, as shown here:
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
. Finally, we
print the value in result
, which in this case is 20
.
You can also return
from inside a loop. While break
only exits the current
loop, return
always exits the current function.
Note: the semicolon after
break counter * 2
is technically optional.break
is very similar toreturn
, in that both can optionally take an expression as an argument, both cause a change in control flow. Code after abreak
orreturn
is never executed, so the Rust compiler treats abreak
expression and areturn
expression as having the value unit, or()
.
Loop Labels 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. Here’s an example with two nested loops:
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}"); }
The outer loop has the label 'counting_up
, and it will count up from 0 to 2.
The inner loop without a label counts down from 10 to 9. The first break
that
doesn’t specify a label will exit the inner loop only. The break 'counting_up;
statement will exit the outer loop. This code prints:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
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
; you could
try that now in a program, if you’d like. However, this pattern is so common
that Rust has a built-in language construct for it, called a while
loop. In
Listing 3-3, we use while
to loop the program three times, counting down each
time, and then, after the loop, print a message and exit.
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 also use the while
construct to loop over the elements of a
collection, such as an array. For example, the loop in Listing 3-4 prints each
element in the array a
.
Here, the code counts up through the elements in the array. It starts at index
0
, and then loops until it reaches the final index in the array (that is,
when index < 5
is no longer true
). Running this code will print every
element in the array:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
All five array values appear in the terminal, as expected. Even though index
will reach a value of 5
at some point, the loop stops executing before trying
to fetch a sixth value from the array.
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
, the code would panic. It’s also slow, because
the compiler adds runtime code to perform the conditional check of whether the
index is within the bounds of the array 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. A for
loop looks like the code in Listing 3-5.
When we run this code, we’ll see the same output as in Listing 3-4. More importantly, we’ve now increased the safety of the code and eliminated the chance of bugs that might result from going beyond the end of the array or not going far enough and missing some items.
Using the for
loop, you wouldn’t need to remember to change any other code if
you changed the number of values in the array, as you would with the method
used in Listing 3-4.
The safety and conciseness of for
loops make them the most commonly used loop
construct in Rust. Even in situations in which you want to run some code a
certain number of times, as in the countdown example that used a while
loop
in Listing 3-3, most Rustaceans would use a for
loop. The way to do that
would be to use a Range
, provided by the standard library, which generates
all numbers in sequence starting from one number and ending before another
number.
Here’s what the countdown would look like using a for
loop and another method
we’ve not yet talked about, rev
, to reverse the range:
Filename: src/main.rs
fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); }
This code is a bit nicer, isn’t it?
Summary
You made it! This was a sizable chapter: you learned about variables, scalar
and compound data types, functions, comments, if
expressions, and loops! To
practice with the concepts discussed in this chapter, try building programs to
do the following:
- Convert temperatures between Fahrenheit and Celsius.
- Generate the nth Fibonacci number.
- Print the lyrics to the Christmas carol “The Twelve Days of Christmas,” taking advantage of the repetition in the song.
When you’re ready to move on, we’ll talk about a concept in Rust that doesn’t commonly exist in other programming languages: ownership.
Understanding 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. In this chapter, we’ll talk about ownership as well as several related features: borrowing, slices, and how Rust lays data out in memory.
What Is Ownership?
Ownership is a discipline for ensuring the safety of Rust programs. To understand ownership, we first need to understand what makes a Rust program safe (or unsafe).
Safety is the Absence of Undefined Behavior
Let’s start with an example. This program is safe to execute:
fn read(y: bool) { if y { println!("y is true!"); } } fn main() { let x = true; read(x); }
We can make this program unsafe to execute by moving the call to read
before the definition of x
:
fn read(y: bool) {
if y {
println!("y is true!");
}
}
fn main() {
read(x); // oh no! x isn't defined!
let x = true;
}
Note: in this chapter, we will use many code examples that do not compile. Make sure to look for the question mark crab if you are not sure whether a program should compile or not.
This second program is unsafe because read(x)
expects x
to have a value of type bool
, but x
doesn’t have a value yet.
When a program like this is executed by an interpreter, then reading x
before it’s defined would raise an exception such as Python’s NameError
or Javascript’s ReferenceError
. But exceptions come at a cost. Each time an interpreted program reads a variable, then the interpreter must check whether that variable is defined.
Rust’s goal is to compile programs into efficient binaries that require as few runtime checks as possible. Therefore Rust does not check at runtime whether a variable is defined before being used. Instead, Rust checks at compile-time. If you try to compile the unsafe program, you will get this error:
error[E0425]: cannot find value `x` in this scope
--> src/main.rs:8:10
|
8 | read(x); // oh no! x isn't defined!
| ^ not found in this scope
You probably have the intuition that it’s good for Rust to ensure that variables are defined before they are used. But why? To justify the rule, we have to ask: what would happen if Rust allowed a rejected program to compile?
Let’s first consider how the safe program compiles and executes. On a computer with a processor using an x86 architecture, Rust generates the following assembly code for the main
function in the safe program (see the full assembly code here):
main:
; ...
mov edi, 1
call read
; ...
Note: if you aren’t familiar with assembly code, that’s ok! This section contains a few examples of assembly just to show you how Rust actually works under the hood. You don’t generally need to know assembly to understand Rust.
This assembly code will:
- Move the number 1, representing
true
, into a “register” (a kind of assembly variable) callededi
. - Call the
read
function, which expects its first argumenty
to be in theedi
register.
If the unsafe function was allowed to compile, its assembly might look like this:
main:
; ...
call read
mov edi, 1 ; mov is after call
; ...
This program is unsafe because read
will expect edi
to be a boolean, which is either the number 0
or 1
. But edi
could be anything: 2
, 100
, 0x1337BEEF
. When read
wants to use its argument y
for any purpose, it will immediately cause UNDEFINED BEHAVIOR!
Rust doesn’t specify what happens if you try to run if y { .. }
when y
isn’t true
or false
. That behavior, or what happens after executing the instruction, is undefined. Something will happen, for example:
- The code executes without crashing, and no one notices a problem.
- The code immediately crashes due to a segmentation fault or another kind of operating system error.
- The code executes without crashing, until a malicious actor creates the right input to delete your production database, overwrite your backups, and steal your lunch money.
A foundational goal of Rust is to ensure that your programs never have undefined behavior. That is the meaning of “safety.” Undefined behavior is especially dangerous for low-level programs with direct access to memory. About 70% of reported security vulnerabilities in low-level systems are caused by memory corruption, which is one form of undefined behavior.
A secondary goal of Rust is to prevent undefined behavior at compile-time instead of run-time. This goal has two motivations:
- Catching bugs at compile-time means avoiding those bugs in production, improving the reliability of your software.
- Catching bugs at compile-time means fewer runtime checks for those bugs, improving the performance of your software.
Rust cannot prevent all bugs. If an application exposes a public and unauthenticated /delete-production-database
endpoint, then a malicious actor doesn’t need a suspicious if-statement to delete the database. But Rust’s protections are still likely to make programs safer versus using a language with fewer protections, e.g. as found by Google’s Android team.
Ownership as a Discipline for Memory Safety
Since safety is the absence of undefined behavior, and since ownership is about safety, then we need to understand ownership in terms of the undefined behaviors it prevents. The Rust Reference maintains a large list of “Behavior considered undefined”. For now, we will focus on one category: operations on memory.
Memory is the space where data is stored during the execution of a program. There are many ways to think about memory:
- If you are unfamiliar with systems programming, you might think of memory at a high level like “memory is the RAM in my computer” or “memory is the thing that runs out if I load too much data”.
- If you are familiar with systems programming, you might think of memory at a low level like “memory is an array of bytes” or “memory is the pointers I get back from
malloc
”.
Both of these memory models are valid, but they are not useful ways to think about how Rust works. The high-level model is too abstract to explain how Rust works. You will need to understand the concept of a pointer, for instance. The low-level model is too concrete to explain how Rust works. Rust does not allow you to interpret memory as an array of bytes, for instance.
Rust provides a particular way to think about memory. Ownership is a discipline for safely using memory within that way of thinking. The rest of this chapter will explain the Rust model of memory.
Variables Live in the Stack
Here’s a program like the one you saw in Section 3.3 that defines a number n
and calls a function plus_one
on n
. Beneath the program is a new kind of diagram. This diagram visualizes the contents of memory during the program’s execution at the three marked points.
Variables live in frames. A frame is a mapping from variables to values within a single scope, such as a function. For example:
- The frame for
main
at location L1 holdsn = 5
. - The frame for
plus_one
at L2 holdsx = 5
. - The frame for
main
at location L3 holdsn = 5; y = 6
.
Frames are organized into a stack of currently-called-functions. For example, at L2 the frame for main
sits above the frame for the called function plus_one
. After a function returns, Rust deallocates the function’s frame. (Deallocation is also called freeing or dropping, and we use those terms interchangeably.) This sequence of frames is called a stack because the most recent frame added is always the next frame freed.
Note: this memory model does not fully describe how Rust actually works! As we saw earlier with the assembly code, the Rust compiler might put
n
orx
into a register rather than a stack frame. But that distinction is an implementation detail. It shouldn’t change your understanding of safety in Rust, so we can focus on the simpler case of frame-only variables.
When an expression reads a variable, the variable’s value is copied from its slot in the stack frame. For example, if we run this program:
The value of a
is copied into b
, and a
is left unchanged, even after changing b
.
Boxes Live in the Heap
However, copying data can take up a lot of memory. For example, here’s a slightly different program. This program copies an array with 1 million elements:
Observe that copying a
into b
causes the main
frame to contain 2 million elements.
To transfer access to data without copying it, Rust uses pointers. A pointer is a value that describes a location in memory. The value that a pointer points-to is called its pointee. One common way to make a pointer is to allocate memory in the heap. The heap is a separate region of memory where data can live indefinitely. Heap data is not tied to a specific stack frame. Rust provides a construct called Box
for putting data on the heap. For example, we can wrap the million-element array in Box::new
like this:
Observe that now, there is only ever a single array at a time. At L1, the value of a
is a pointer (represented by dot with an arrow) to the array inside the heap. The statement let b = a
copies the pointer from a
into b
, but the pointed-to data is not copied. Note that a
is now grayed out because it has been moved — we will see what that means in a moment.
Rust Does Not Permit Manual Memory Management
Memory management is the process of allocating memory and deallocating memory. In other words, it’s the process of finding unused memory and later returning that memory when it is no longer used. Stack frames are automatically managed by Rust. When a function is called, Rust allocates a stack frame for the called function. When the call ends, Rust deallocates the stack frame.
As we saw above, heap data is allocated when calling Box::new(..)
. But when is heap data deallocated? Imagine that Rust had a free()
function that frees a heap allocation. Imagine that Rust let a programmer call free
whenever they wanted. This kind of “manual” memory management easily leads to bugs. For example, we could read a pointer to freed memory:
Note: you may wonder how we are executing this Rust program that doesn’t compile. We use special tools to simulate Rust as if the borrow checker were disabled, for educational purposes. That way we can answer what-if questions, like: what if Rust let this unsafe program compile?
Here, we allocate an array on the heap. Then we call free(b)
, which deallocates the heap memory of b
. Therefore the value of b
is a pointer to invalid memory, which we represent as the “⦻” icon. No undefined behavior has happened yet! The program is still safe at L2. It’s not necessarily a problem to have an invalid pointer.
The undefined behavior happens when we try to use the pointer by reading b[0]
. That would attempt to access invalid memory, which could cause the program to crash. Or worse, it could not crash and return arbitrary data. Therefore this program is unsafe.
Rust does not allow programs to manually deallocate memory. That policy avoids the kinds of undefined behaviors shown above.
A Box’s Owner Manages Deallocation
Instead, Rust automatically frees a box’s heap memory. Here is an almost correct description of Rust’s policy for freeing boxes:
Box deallocation principle (almost correct): If a variable is bound to a box, when Rust deallocates the variable’s frame, then Rust deallocates the box’s heap memory.
For example, let’s trace through a program that allocates and frees a box:
At L1, before calling make_and_drop
, the state of memory is just the stack frame for main
. Then at L2, while calling make_and_drop
, a_box
points to 5
on the heap. Once make_and_drop
is finished, Rust deallocates its stack frame. make_and_drop
contains the variable a_box
, so Rust also deallocates the heap data in a_box
. Therefore the heap is empty at L3.
The box’s heap memory has been successfully managed. But what if we abused this system? Returning to our earlier example, what happens when we bind two variables to a box?
fn main() {
let a = Box::new([0; 1_000_000]);
let b = a;
}
The boxed array has now been bound to both a
and b
. By our “almost correct” principle, Rust would try to free the box’s heap memory twice on behalf of both variables. That’s undefined behavior too!
To avoid this situation, we finally arrive at ownership. When a
is bound to Box::new([0; 1_000_000])
, we say that a
owns the box. The statement let b = a
moves ownership of the box from a
to b
. Given these concepts, Rust’s policy for freeing boxes is more accurately described as:
Box deallocation principle (fully correct): If a variable owns a box, when Rust deallocates the variable’s frame, then Rust deallocates the box’s heap memory.
In the example above, b
owns the boxed array. Therefore when the scope ends, Rust deallocates the box only once on behalf of b
, not a
.
Collections Use Boxes
Boxes are used by Rust data structures1 like Vec
, String
, and HashMap
to hold a variable number of elements. For example, here’s a program that creates, moves, and mutates a string:
This program is more involved, so make sure you follow each step:
- At L1, the string “Ferris” has been allocated on the heap. It is owned by
first
. - At L2, the function
add_suffix(first)
has been called. This moves ownership of the string fromfirst
toname
. The string data is not copied, but the pointer to the data is copied. - At L3, the function
name.push_str(" Jr.")
resizes the string’s heap allocation. This does three things. First, it creates a new larger allocation. Second, it writes “Ferris Jr.” into the new allocation. Third, it frees the original heap memory.first
now points to deallocated memory. - At L4, the frame for
add_suffix
is gone. This function returnedname
, transferring ownership of the string tofull
.
Variables Cannot Be Used After Being Moved
The string program helps illustrate a key safety principle for ownership. Imagine that first
were used in main
after calling add_suffix
. We can simulate such a program and see the undefined behavior that results:
first
points to deallocated memory after calling add_suffix
. Reading first
in println!
would therefore be a violation of memory safety (undefined behavior). Remember: it’s not a problem that first
points to deallocated memory. It’s a problem that we tried to use first
after it became invalid.
Thankfully, Rust will refuse to compile this program, giving the following error:
error[E0382]: borrow of moved value: `first`
--> test.rs:4:35
|
2 | let first = String::from("Ferris");
| ----- move occurs because `first` has type `String`, which does not implement the `Copy` trait
3 | let full = add_suffix(first);
| ----- value moved here
4 | println!("{full}, originally {first}"); // first is now used here
| ^^^^^ value borrowed here after move
Let’s walk through the steps of this error. Rust says that first
is moved when we called add_suffix(first)
on line 3. The error clarifies that first
is moved because it has type String
, which does not implement Copy
. We will discuss Copy
soon — in brief, you would not get this error if you used an i32
instead of String
. Finally, the error says that we use first
after being moved (it’s “borrowed”, which we discuss next section).
So if you move a variable, Rust will stop you from using that variable later. More generally, the compiler will enforce this principle:
Moved heap data principle: if a variable
x
moves ownership of heap data to another variabley
, thenx
cannot be used after the move.
Now you should start to see the relationship between ownership, moves, and safety. Moving ownership of heap data avoids undefined behavior from reading deallocated memory.
Cloning Avoids Moves
One way to avoid moving data is to clone it using the .clone()
method. For example, we can fix the safety issue in the previous program with a clone:
Observe that at L1, first_clone
did not “shallow” copy the pointer in first
, but instead “deep” copied the string data into a new heap allocation. Therefore at L2, while first_clone
has been moved and invalidated by add_suffix
, the original first
variable is unchanged. It is safe to continue using first
.
Summary
Ownership is primarily a discipline of heap management:2
- All heap data must be owned by exactly one variable.
- Rust deallocates heap data once its owner goes out of scope.
- Ownership can be transferred by moves, which happen on assignments and function calls.
- Heap data can only be accessed through its current owner, not a previous owner.
We have emphasized not just how Rust’s safeguards work, but why they avoid undefined behavior. When you get an error message from the Rust compiler, it’s easy to get frustrated if you don’t understand why Rust is complaining. These conceptual foundations should help you with interpreting Rust’s error messages. They should also help you design more Rustic APIs.
These data structures don’t use the literal Box
type. For example, String
is implemented with Vec
, and Vec
is implemented with RawVec
rather than Box
. But types like RawVec
are still box-like: they own memory in the heap.
In another sense, ownership is a discipline of pointer management. But we haven’t described yet about how to create pointers to anywhere other than the heap. We’ll get there in the next section.
References and Borrowing
Ownership, boxes, and moves provide a foundation for safely programming with the heap. However, move-only APIs can be inconvenient to use. For example, say you want to read some strings twice:
In this example, calling greet
moves the data from m1
and m2
into the parameters of greet
. Both strings are dropped at the end of greet
, and therefore cannot be used within main
. If we try to read them like in the operation format!(..)
, then that would be undefined behavior. The Rust compiler therefore rejects this program with the same error we saw last section:
error[E0382]: borrow of moved value: `m1`
--> test.rs:5:30
(...rest of the error...)
This move behavior is extremely inconvenient. Programs often need to use a string more than once. An alternative greet
could return ownership of the strings, like this:
However, this style of program is quite verbose. Rust provides a concise style of reading and writing without moves through references.
References Are Non-Owning Pointers
A reference is a kind of pointer. Here’s an example of a reference that rewrites our greet
program in a more convenient manner:
The expression &m1
uses the ampersand operator to create a reference to (or “borrow”) m1
. The type of the greet
parameter g1
is changed to &String
, meaning “a reference to a String
”.
Observe at L2 that there are two steps from g1
to the string “Hello”. g1
is a reference that points to m1
on the stack, and m1
is a String containing a box that points to “Hello” on the heap.
While m1
owns the heap data “Hello”, g1
does not own either m1
or “Hello”. Therefore after greet
ends and the program reaches L3, no heap data has been deallocated. Only the stack frame for greet
disappears. This fact is consistent with our Box Deallocation Principle. Because g1
did not own “Hello”, Rust did not deallocate “Hello” on behalf of g1
.
References are non-owning pointers, because they do not own the data they point to.
Dereferencing a Pointer Accesses Its Data
The previous examples using boxes and strings have not shown how Rust “follows” a pointer to its data. For example, the println!
macro has mysteriously worked for both owned strings of type String
, and for string references of type &String
. The underlying mechanism is the dereference operator, written with an asterisk (*
). For example, here’s a program that uses dereferences in a few different ways:
Observe the difference between r1
pointing to x
on the stack, and r2
pointing to the heap value 2
.
You probably won’t see the dereference operator very often when you read Rust code. Rust implicitly inserts dereferences and references in certain cases, such as calling a method with the dot operator. For example, this program shows two equivalent ways of calling the i32::abs
(absolute value) and str::len
(string length) functions:
fn main() {
let x: Box<i32> = Box::new(-1);
let x_abs1 = i32::abs(*x); // explicit dereference
let x_abs2 = x.abs(); // implicit dereference
assert_eq!(x_abs1, x_abs2);
let r: &Box<i32> = &x;
let r_abs1 = i32::abs(**r); // explicit dereference (twice)
let r_abs2 = r.abs(); // implicit dereference (twice)
assert_eq!(r_abs1, r_abs2);
let s = String::from("Hello");
let s_len1 = str::len(&s); // explicit reference
let s_len2 = s.len(); // implicit reference
assert_eq!(s_len1, s_len2);
}
This example shows implicit conversions in three ways:
-
The
i32::abs
function expects an input of typei32
. To callabs
with aBox<i32>
, you can explicitly dereference the box likei32::abs(*x)
. You can also implicitly dereference the box using method-call syntax likex.abs()
. The dot syntax is syntactic sugar for the function-call syntax. -
This implicit conversion works for multiple layers of pointers. For example, calling
abs
on a reference to a boxr: &Box<i32>
will insert two dereferences. -
This conversion also works the opposite direction. The function
str::len
expects a reference&str
. If you calllen
on an ownedString
, then Rust will insert a single borrowing operator. (In fact, there is a further conversion fromString
tostr
!)
We will say more about method calls and implicit conversions in later chapters. For now, the important takeaway is that these conversions are happening with method calls and some macros like println
. We want to unravel all the “magic” of Rust so you can have a clear mental model of how Rust works.
Rust Avoids Simultaneous Aliasing and Mutation
Pointers are a powerful and dangerous feature because they enable aliasing. Aliasing is accessing the same data through different variables. On its own, aliasing is harmless. But combined with mutation, we have a recipe for disaster. One variable can “pull the rug out” from another variable in many ways, for example:
- By deallocating the aliased data, leaving the other variable to point to deallocated memory.
- By mutating the aliased data, invalidating runtime properties expected by the other variable.
- By concurrently mutating the aliased data, causing a data race with nondeterministic behavior for the other variable.
As a running example, we are going to look at programs using the vector data structure, Vec
. Unlike arrays which have a fixed length, vectors have a variable length by storing their elements in the heap. For example, Vec::push
adds an element to the end of a vector, like this:
The macro vec!
creates a vector with the elements between the brackets. The vector v
has type Vec<i32>
. The syntax <i32>
means the elements of the vector have type i32
.
One important implementation detail is that v
allocates a heap array of a certain capacity. We can peek into Vec
’s internals and see this detail for ourselves:
Note: click the binocular icon in the top right of the diagram to toggle this detailed view in any runtime diagram.
Notice that the vector has a length (len
) of 3 and a capacity (cap
) of 3. The vector is at capacity. So when we do a push
, the vector has to create a new allocation with larger capacity, copy all the elements over, and deallocate the original heap array. In the diagram above, the array 1 2 3 4
is in a (potentially) different memory location than the original array 1 2 3
.
To tie this back to memory safety, let’s bring references into the mix. Say we created a reference to a vector’s heap data. Then that reference can be invalidated by a push, as simulated below:
Initially, v
points to an array with 3 elements on the heap. Then num
is created as a reference to the third element, as seen at L1. However, the operation v.push(4)
resizes v
. The resize will deallocate the previous array and allocate a new, bigger array. In the process, num
is left pointing to invalid memory. Therefore at L3, dereferencing *num
reads invalid memory, causing undefined behavior.
In more abstract terms, the issue is that the vector v
is both aliased (by the reference num
) and mutated (by the operation v.push(4)
). So to avoid these kinds of issues, Rust follows a basic principle:
Pointer Safety Principle: data should never be aliased and mutated at the same time.
Data can be aliased. Data can be mutated. But data cannot be both aliased and mutated. For example, Rust enforces this principle for boxes (owned pointers) by disallowing aliasing. Assigning a box from one variable to another will move ownership, invalidating the previous variable. Owned data can only be accessed through the owner — no aliases.
However, because references are non-owning pointers, they need different rules than boxes to ensure the Pointer Safety Principle. By design, references are meant to temporarily create aliases. In the rest of this section, we will explain the basics of how Rust ensures the safety of references through the borrow checker.
References Change Permissions on Places
The core idea behind the borrow checker is that variables have three kinds of permissions on their data:
- Read (R): data can be copied to another location.
- Write (W): data can be mutated.
- Own (O): data can be moved or dropped.
These permissions don’t exist at runtime, only within the compiler. They describe how the compiler “thinks” about your program before the program is executed.
By default, a variable has read/own permissions (RO) on its data. If a variable is annotated with let mut
, then it also has the write permission (W). The key idea is
that references can temporarily remove these permissions.
To illustrate this idea, let’s look at the permissions on a variation of the program above that is actually safe. The push
has been moved after the println!
. The permissions in this program are visualized with a new kind of diagram. The diagram shows the changes in permissions on each line.
Let’s walk through each line:
- After
let mut v = (...)
, the variablev
has been initialized (indicated by ). It gains +R+W+O permissions (the plus sign indicates gain). - After
let num = &v[2]
, the data inv
has been borrowed bynum
(indicated by ). Three things happen:- The borrow removes WO permissions from
v
(the slash indicates loss).v
cannot be written or owned, but it can still be read. - The variable
num
has gained RO permissions.num
is not writable (the missing W permission is shown as a dash ‒) because it was not markedlet mut
. - The place
*num
has gained the R permission.
- The borrow removes WO permissions from
- After
println!(...)
, thennum
is no longer in use, sov
is no longer borrowed. Therefore:v
regains its WO permissions (indicated by ).num
and*num
have lost all of their permissions (indicated by ).
- After
v.push(4)
, thenv
is no longer in use, and it loses all of its permissions.
Next, let’s explore a few nuances of the diagram. First, why do you see both num
and *num
? Because accessing data through a reference is not the same as manipulating the reference itself. For example, say we declared a reference to a number with let mut
:
Notice that x_ref
has the W permission, while *x_ref
does not. That means we can assign a different reference to the x_ref
variable (e.g. x_ref = &y
), but we cannot mutate the data it points to (e.g. *x_ref += 1
).
More generally, permissions are defined on places and not just variables. A place is anything you can put on the left-hand side of an assignment. Places include:
- Variables, like
a
. - Dereferences of places, like
*a
. - Array accesses of places, like
a[0]
. - Fields of places, like
a.0
for tuples ora.field
for structs (discussed next chapter). - Any combination of the above, like
*((*a)[0].1)
.
Second, why do places lose permissions when they become unused? Because some permissions are mutually exclusive. If you write num = &v[2]
, then v
cannot be mutated or dropped while num
is in use. But that doesn’t mean it’s invalid to use num
again. For example, if we add another println!
to the above program, then num
simply loses its permissions one line later:
It’s only a problem if you attempt to use num
again after mutating v
. Let’s look at this in more detail.
The Borrow Checker Finds Permission Violations
Recall the Pointer Safety Principle: data should not be aliased and mutated. The goal of these permissions is to ensure that data cannot be mutated if it is aliased. Creating a reference to data (“borrowing” it) causes that data to be temporarily read-only until the reference is no longer in use.
Rust uses these permissions in its borrow checker. The borrow checker looks for potentially unsafe operations involving references. Let’s return to the unsafe program we saw earlier, where push
invalidates a reference. This time we’ll add another aspect to the permissions diagram:
Any time a place is used, Rust expects that place to have certain permissions depending on the operation. For example, the borrow &v[2]
requires that v
is readable. Therefore the R permission is shown between the operation &
and the place v
. The letter is filled-in because v
has the read permission at that line.
By contrast, the mutating operation v.push(4)
requires that v
is readable and writable. Both R and W are shown. However, v
does not have write permissions (it is borrowed by num
). So the letter W is hollow, indicating that the write permission is expected but v
does not have it.
If you try to compile this program, then the Rust compiler will return the following error:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> test.rs:4:1
|
3 | let num: &i32 = &v[2];
| - immutable borrow occurs here
4 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("Third element is {}", *num);
| ---- immutable borrow later used here
The error message explains that v
cannot be mutated while the reference num
is in use. That’s the surface-level reason — the underlying issue is that num
could be invalidated by push
. Rust catches that potential violation of memory safety.
Mutable References Provide Unique and Non-Owning Access to Data
The references we have seen so far are read-only immutable references (also called shared references). Immutable references permit aliasing but disallow mutation. However, it is also useful to temporarily provide mutable access to data without moving it.
The mechanism for this is mutable references (also called unique references). Here’s a simple example of a mutable reference with the accompanying permissions changes:
Note: when the expected permissions are not strictly relevant to an example, we will abbreviate them as dots like. You can hover your mouse over the circles (or tap on a touchscreen) to see the corresponding permission letters.
A mutable reference is created with the &mut
operator. The type of num
is written as &mut i32
. Compared to immutable references, you can see two important differences in the permissions:
- When
num
was an immutable reference,v
still had the R permission. Now thatnum
is a mutable reference,v
has lost all permissions whilenum
is in use. - When
num
was an immutable reference, the place*num
only had the R permission. Now thatnum
is a mutable reference,*num
has also gained the W permission.
The first observation is what makes mutable references safe. Mutable references allow mutation but prevent aliasing. The borrowed place v
becomes temporarily unusable, so effectively not an alias.
The second observation is what makes mutable references useful. v[2]
can be mutated through *num
. For example, *num += 1
mutates v[2]
. Note that *num
has the W permission, but num
does not. num
refers to the mutable reference itself, e.g. num
cannot be reassigned to a different mutable reference.
Mutable references can also be temporarily “downgraded” to read-only references. For example:
Note: when permission changes are not relevant to an example, we will hide them. You can view hidden steps by clicking “»”, and you can view hidden permissions within a step by clicking “● ● ●”.
In this program, the borrow &*num
removes the W permission from *num
but not the R permission, so println!(..)
can read both *num
and *num2
.
Permissions Are Returned At The End of a Reference’s Lifetime
We said above that a reference changes permissions while it is “in use”. The phrase “in use” is describing a reference’s lifetime, or the range of code spanning from its birth (where the reference is created) to its death (the last time(s) the reference is used).
For example, in this program, the lifetime of y
starts with let y = &x
, and ends with let z = *y
:
The W permission on x
is returned to x
after the lifetime of y
has ended, like we have seen before.
In the previous examples, a lifetime has been a contiguous region of code. However, once we introduce control flow, this is not necessarily the case. For example, here is a function that capitalizes the first character in a vector of ASCII characters:
The variable c
has a different lifetime in each branch of the if-statement. In the then-block, c
is used in the expression c.to_ascii_uppercase()
. Therefore *v
does not regain the W permission until after that line.
However, in the else-block, c
is not used. *v
immediately regains the W permission on entry to the else-block.
Data Must Outlive All Of Its References
As a part of the Pointer Safety Principle, the borrow checker enforces that data must outlive any references to it. Rust enforces this property in two ways. The first way deals with references that are created and dropped within the scope of a single function. For example, say we tried to drop a string while holding a reference to it:
To catch these kinds of errors, Rust uses the permissions we’ve already discussed. The borrow &s
removes the O permission from s
. However, drop
expects the O permission, leading to a permission mismatch.
The key idea is that in this example, Rust knows how long s_ref
lives. But Rust needs a different enforcement mechanism when it doesn’t know how long a reference lives. Specifically, when references are either input to a function, or output from a function. For example, here is a safe function that returns a reference to the first element in a vector:
This snippet introduces a new kind of permission, the flow permission F. The F permission is expected whenever an expression uses an input reference (like &strings[0]
), or returns an output reference (like return s_ref
).
Unlike the RWO permissions, F does not change throughout the body of a function. A reference has the F permission if it’s allowed to be used (that is, to flow) in a particular expression. For example, let’s say we change first
to a new function first_or
that includes a default
parameter:
This function no longer compiles, because the expressions &strings[0]
and default
lack the necessary F permission to be returned. But why? Rust gives the following error:
error[E0106]: missing lifetime specifier
--> test.rs:1:57
|
1 | fn first_or(strings: &Vec<String>, default: &String) -> &String {
| ------------ ------- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `strings` or `default`
The message “missing lifetime specifier” is a bit mysterious, but the help message provides some useful context. If Rust just looks at the function signature, it doesn’t know whether the output &String
is a reference to either strings
or default
. To understand why that matters, let’s say we used first_or
like this:
fn main() {
let strings = vec![];
let default = String::from("default");
let s = first_or(&strings, &default);
drop(default);
println!("{}", s);
}
This program is unsafe if first_or
allows default
to flow into the return value. Like the previous example, drop
could invalidate s
. Rust would only allow this program to compile if it was certain that default
cannot flow into the return value.
To specify whether default
can be returned, Rust provides a mechanism called lifetime parameters. We will explain that feature later in Chapter 10.3, “Validating References with Lifetimes”. For now, it’s enough to know that: (1) input/output references are treated differently than references within a function body, and (2) Rust uses a different mechanism, the F permission, to check the safety of those references.
To see the F permission in another context, say you tried to return a reference to a variable on the stack like this:
This program is unsafe because the reference &s
will be invalidated when return_a_string
returns. And Rust will reject this program with a similar missing lifetime specifier
error. Now you can understand that error means that s_ref
is missing the appropriate flow permissions.
Summary
References provide the ability to read and write data without consuming ownership of it. References are created with borrows (&
and &mut
) and used with dereferences (*
), often implicitly.
However, references can be easily misused. Rust’s borrow checker enforces a system of permissions that ensures references are used safely:
- All variables can read, own, and (optionally) write their data.
- Creating a reference will transfer permissions from the borrowed place to the reference.
- Permissions are returned once the reference’s lifetime has ended.
- Data must outlive all references that point to it.
In this section, it probably feels like we’ve described more of what Rust cannot do than what Rust can do. That is intentional! One of Rust’s core features is allowing you to use pointers without garbage collection, while also avoiding undefined behavior. Understanding these safety rules now will help you avoid frustration with the compiler later.
Fixing Ownership Errors
Learning how to fix an ownership error is a core Rust skill. When the borrow checker rejects your code, how should you respond? In this section, we will discuss several case studies of common ownership errors. Each case study will present a function rejected by the compiler. Then we will explain why Rust rejects the function, and show several ways to fix it.
A common theme will be understanding whether a function is actually safe or unsafe. Rust will always reject an unsafe program1. But sometimes, Rust will also reject a safe program. These case studies will show how to respond to errors in both situations.
Fixing an Unsafe Program: Returning a Reference to the Stack
Our first case study is about returning a reference to the stack, just like we discussed last section in “Data Must Outlive All Of Its References”. Here’s the function we looked at:
fn return_a_string() -> &String {
let s = String::from("Hello world");
&s
}
When thinking about how to fix this function, we need to ask: why is this program unsafe? Here, the issue is with the lifetime of the referred data. If you want to pass around a reference to a string, you have to make sure that the underlying string lives long enough.
Depending on the situation, here are four ways you can extend the lifetime of the string. One is to move ownership of the string out of the function, changing &String
to String
:
#![allow(unused)] fn main() { fn return_a_string() -> String { let s = String::from("Hello world"); s } }
Another possibility is to return a string literal, which lives forever (indicated by 'static
). This solution applies if we never intend to change the string, and then a heap allocation is unnecessary:
#![allow(unused)] fn main() { fn return_a_string() -> &'static str { "Hello world" } }
Another possibility is to defer borrow-checking to runtime by using garbage collection. For example, you can use a reference-counted pointer:
#![allow(unused)] fn main() { use std::rc::Rc; fn return_a_string() -> Rc<String> { let s = Rc::new(String::from("Hello world")); Rc::clone(&s) } }
We will discuss reference-counting more in Chapter 15.4 “Rc<T>
, the Reference Counted Smart Pointer”. In short, Rc::clone
only clones a pointer to s
and not the data itself. At runtime, the Rc
checks when the last Rc
pointing to data has been dropped, and then deallocates the data.
Yet another possibility is to have the caller provide a “slot” to put the string using a mutable reference:
#![allow(unused)] fn main() { fn return_a_string(output: &mut String) { output.replace_range(.., "Hello world"); } }
With this strategy, the caller is responsible for creating space for the string. This style can be verbose, but it can also be more memory-efficient if the caller needs to carefully control when allocations occur.
Which strategy is most appropriate will depend on your application. But the key idea is to recognize the root issue underlying the surface-level ownership error. How long should my string live? Who should be in charge of deallocating it? Once you have a clear answer to those questions, then it’s a matter of changing your API to match.
Fixing an Unsafe Program: Not Enough Permissions
Another common issue is trying to mutate read-only data, or trying to drop data behind a reference. For example, let’s say we tried to write a function stringify_name_with_title
. This function is supposed to create a person’s full name from a vector of name parts, including an extra title.
This program is rejected by the borrow checker because name
is an immutable reference, but name.push(..)
requires the W permission. This program is unsafe because push
could invalidate other references to name
outside of stringify_name_with_title
, like this:
In this example, a reference first
to name[0]
is created before calling stringify_name_with_title
. The function name.push(..)
reallocates the contents of name
, which invalidates first
, causing the println
to read deallocated memory.
So how do we fix this API? One straightforward solution is to change the type of name from &Vec<String>
to &mut Vec<String>
:
fn stringify_name_with_title(name: &mut Vec<String>) -> String {
name.push(String::from("Esq."));
let full = name.join(" ");
full
}
But this is not a good solution! Functions should not mutate their inputs if the caller would not expect it. A person calling stringify_name_with_title
probably does not expect their vector to be modified by this function. Another function like add_title_to_name
might be expected to mutate its input, but not our function.
Another option is to take ownership of the name, by changing &Vec<String>
to Vec<String>
:
fn stringify_name_with_title(mut name: Vec<String>) -> String {
name.push(String::from("Esq."));
let full = name.join(" ");
full
}
But this is also not a good solution! It is very rare for Rust functions to take ownership of heap-owning data structures like Vec
and String
. This version of stringify_name_with_title
would make the input name
unusable, which is very annoying to a caller as we discussed at the beginning of “References and Borrowing”.
So the choice of &Vec
is actually a good one, which we do not want to change. Instead, we can change the body of the function. There are many possible fixes which vary in how much memory they use. One possibility is to clone the input name
:
fn stringify_name_with_title(name: &Vec<String>) -> String {
let mut name_clone = name.clone();
name_clone.push(String::from("Esq."));
let full = name_clone.join(" ");
full
}
By cloning name
, we are allowed to mutate the local copy of the vector. However, the clone copies every string in the input. We can avoid unnecessary copies by adding the suffix later:
fn stringify_name_with_title(name: &Vec<String>) -> String {
let mut full = name.join(" ");
full.push_str(" Esq.");
full
}
This solution works because slice::join
already copies the data in name
into the string full
.
In general, writing Rust functions is a careful balance of asking for the right level of permissions. For this example, it’s most idiomatic to only expect the read permission on name
.
Fixing an Unsafe Program: Aliasing and Mutating a Data Structure
Another unsafe operation is using a reference to heap data that gets deallocated by another alias. For example, here’s a function that gets a reference to the largest string in a vector, and then uses it while mutating the vector:
Note: this example uses iterators and closures to succinctly find a reference to the largest string. We will discuss those features in later chapters, and for now we will provide an intuitive sense of how the features work here.
This program is rejected by the borrow checker because let largest = ..
removes the W permissions on dst
. However, dst.push(..)
requires the W permission. Again, we should ask: why is this program unsafe? Because dst.push(..)
could deallocate the contents of dst
, invalidating the reference largest
.
To fix the program, the key insight is that we need to shorten the lifetime of largest
to not overlap with dst.push(..)
. One possibility is to clone largest
:
#![allow(unused)] fn main() { fn add_big_strings(dst: &mut Vec<String>, src: &[String]) { let largest: String = dst.iter().max_by_key(|s| s.len()).unwrap().clone(); for s in src { if s.len() > largest.len() { dst.push(s.clone()); } } } }
However, this may cause a performance hit for allocating and copying the string data.
Another possibility is to perform all the length comparisons first, and then mutate dst
afterwards:
#![allow(unused)] fn main() { fn add_big_strings(dst: &mut Vec<String>, src: &[String]) { let largest: &String = dst.iter().max_by_key(|s| s.len()).unwrap(); let to_add: Vec<String> = src.iter().filter(|s| s.len() > largest.len()).cloned().collect(); dst.extend(to_add); } }
However, this also causes a performance hit for allocating the vector to_add
.
A final possibility is to copy out the length of largest
, since we don’t actually need the contents of largest
, just its length.
This solution is arguably the most idiomatic and the most performant:
#![allow(unused)] fn main() { fn add_big_strings(dst: &mut Vec<String>, src: &[String]) { let largest_len: usize = dst.iter().max_by_key(|s| s.len()).unwrap().len(); for s in src { if s.len() > largest_len { dst.push(s.clone()); } } } }
These solutions all share in common the key idea: shortening the lifetime of borrows on dst
to not overlap with a mutation to dst
.
Fixing an Unsafe Program: Copying vs. Moving Out of a Collection
A common confusion for Rust learners happens when copying data out of a collection, like a vector. For example, here’s a safe program that copies a number out of a vector:
The dereference operation *n_ref
expects just the R permission, which the path *n_ref
has. But what happens if we change the type of elements in the vector from i32
to String
? Then it turns out we no longer have the necessary permissions:
The first program will compile, but the second program will not compile. Rust gives the following error message:
error[E0507]: cannot move out of `*s_ref` which is behind a shared reference
--> test.rs:4:9
|
4 | let s = *s_ref;
| ^^^^^^
| |
| move occurs because `*s_ref` has type `String`, which does not implement the `Copy` trait
The issue is that the vector v
owns the string “Hello world”. When we dereference s_ref
, that tries to take ownership of the string from the vector. But references are non-owning pointers — we can’t take ownership through a reference. Therefore Rust complains that we “cannot move out of […] a shared reference”.
But why is this unsafe? We can illustrate the problem by simulating the rejected program: