50 KiB
Up and Running
We’ll start our journey with Rust by talking about the absolute basics — concepts that appear in almost every programming language. Many programming languages have lots in common at their core. None of the concepts presented are unique to Rust, but we’ll cover Rust’s particular syntax and conventions around these common concepts.
If you want to skip this section, you can, but you may end up coming back later to find out small details. These foundations will be in every single useful Rust program, and learning them gives us a strong core to start from.
Variable Bindings
The foundation of virtually every program is the ability to store and modify data. Rust programs are no different. Let’s start with a short example.
The basics of bindings
First, we’ll generate a new project with Cargo. Open a terminal, and navigate to the directory where you’d like to keep your projects. From there, let’s generate a new project:
$ cargo new --bin bindings
$ cd bindings
This creates a new project, ‘bindings’, and sets up our Cargo.toml
and
src/main.rs
files. As we saw in “Hello, World!”, Cargo will generate these
files and create a little ‘hello world’ program for us:
fn main() {
println!("Hello, world!");
}
Let’s replace that program with this one:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
}
And finally, run it:
$ cargo run
Compiling bindings v0.1.0 (file:///projects/bindings)
Running `target/debug/bindings`
The value of x is: 5
If you see an error instead, double check that you have copied the program exactly as written. Let’s break this down, line by line.
fn main() {
The main()
function is the entry point of every Rust program. We’ll talk more
about functions in the next section, but for now, all we need to know is that
this is where our program begins. The opening curly brace, {
, indicates the
start of the function’s body.
let x = 5;
This is our first ‘variable binding’, which we create with a ‘let
statement’.
This let
statement has this form:
let NAME = EXPRESSION;
A let
statement first evaluates the EXPRESSION
, and then binds the
resulting value to NAME
so that it can be referred to later in the program.
In our simple example, the expression was already a value, 5, but we could
achieve the same effect with:
let x = 2 + 3;
In general, let
statements work with patterns; a name is a particularly
humble form of pattern. Patterns are a big part of Rust, we’ll see more complex
and powerful patterns as we go along.
Before we do that, though, let’s finish investigating this example. Here’s the next line:
println!("The value of x is: {}", x);
The println!
macro prints text to the screen. We can tell that it’s a macro
due to the !
. We won’t learn how to write macros until much later in the
book, but we’ll use macros provided by the standard library throughout. Every
time you see a !
, remember that it signifies a macro. Macros can add new
syntax to the language, and the !
is a reminder that things may look slightly
unusual.
println!
, specifically, has one required argument, a ‘format string’, and
zero or more optional arguments. The format string can contain the special text
{}
. Each instance of {}
corresponds to an additional argument. Here’s an
example:
let x = 2 + 3;
let y = x + 5;
println!("The value of x is {}, and the value of y is {}", x, y);
You can think of {}
as little crab pincers, holding the value in place. This
placeholder has a number of more advanced formatting options that we’ll discuss
later.
}
Finally, a closing curly brace matches up with the opening curly brace that
declared the main()
function, and declares its end.
This explains our output:
The value of x is: 5
We assign 5
to a binding, x
, and then print it to the screen with
println!
.
Multiple binding
Let’s try a more complex pattern. Change our example program to this:
fn main() {
let (x, y) = (5, 6);
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
And run it with cargo run
:
$ cargo run
Compiling bindings v0.1.0 (file:///projects/bindings)
Running `target/debug/bindings`
The value of x is: 5
The value of y is: 6
We’ve created two bindings with one let
! Here’s our pattern:
(x, y)
And here’s the value:
(5, 6)
As you can see, the two line up visually, and so let
binds 5
to x
and 6
to y
. We could have used two let
statements as well:
fn main() {
let x = 5;
let y = 6;
}
In simple cases like this, two let
s may be clearer, but in others, creating
multiple bindings at once is nice. As we become more proficient in Rust, we’ll
figure out which style is better, but it’s mostly a judgement call.
Type annotations
You may have noticed that we didn’t declare the type of x
or y
in our
previous examples. Rust is a statically typed language, which means that at
compile time, we must know the types of all bindings. But annotating every
single binding with a type can feel like busywork, and make code noisy. To
solve this issue, Rust uses ‘type inference’, meaning that it attempts to infer
the types of your bindings.
The primary way that the type is inferred is by looking at how it is used. Let’s look at the example again:
fn main() {
let x = 5;
}
When we bind x
to 5
, the compiler knows that x
should be a numeric type.
Without any other information, it defaults to i32
, a thirty-two bit integer
type. We’ll talk more about Rust’s basic types in section 3.3.
Here’s what a let
statement with a ‘type annotation’ looks like:
fn main() {
let x: i32 = 5;
}
We can add a colon, followed by the type name. Here’s the structure of a let
statement with a type annotation:
let PATTERN: TYPE = VALUE;
Note that the colon and the TYPE
go after the PATTERN
, not in the pattern
itself. As an example, here’s our more complex pattern with two bindings:
fn main() {
let (x, y): (i32, i32) = (5, 6);
}
Just like we match up the VALUE
with the PATTERN
, we match up the TYPE
with the PATTERN
.
Delayed Initialization
We do not have to provide bindings with an initial value, and can assign it later. Try this program:
fn main() {
let x;
x = 5;
println!("The value of x is: {}", x);
}
And run it with cargo run
:
$ cargo run
Compiling bindings v0.1.0 (file:///projects/bindings)
Running `target/debug/bindings`
The value of x is: 5
It’s all good. This raises a question, though: what if we try to print out a binding before we declare a value? Here’s a program that demonstrates this question:
fn main() {
let x;
println!("The value of x is: {}", x);
x = 5;
}
We can find out the answer with cargo run
:
Compiling bindings v0.1.0 (file:///projects/bindings)
src/main.rs:4:39: 4:40 error: use of possibly uninitialized variable: `x` [E0381]
src/main.rs:4 println!(“The value of x is: {}”, x);
^
<std macros>:2:25: 2:56 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
src/main.rs:4:5: 4:42 note: in this expansion of println! (defined in <std macros>)
src/main.rs:4:39: 4:40 help: run `rustc --explain E0381` to see a detailed explanation
error: aborting due to previous error
Could not compile `bindings`.
To learn more, run the command again with --verbose.
An error! The compiler won’t let us write a program like this. This is our
first example of the compiler helping us find an error in our program.
Different programming languages have different ways of approaching this
problem. Some languages always initialize values with some sort of default.
Other languages leave the value uninitialized, and make no promises about what
happens if you try to use something before initialization. Rust chooses
something else: error and force the programmer to explain what they want. We
must do some sort of initialization before we can use x
.
Extended error explanations
There’s one more interesting part of this error message:
src/main.rs:4:39: 4:40 help: run `rustc --explain E0381` to see a detailed explanation
We can see an extended explanation by passing the --explain
flag to rustc
.
Not every error has a longer explanation, but many of them do. These extended
explanations try to show off common ways that the error occurs, and common
solutions to the issue. Here’s E0381
:
$ rustc --explain E0381
It is not allowed to use or capture an uninitialized variable. For example:
fn main() {
let x: i32;
let y = x; // error, use of possibly uninitialized variable
To fix this, ensure that any declared variables are initialized before being
used.
These explanations can really help if you’re stuck on an error. The compiler is your friend, and is here to help.
Mutable bindings
What about changing the value of a binding? Here’s another sample program that asks this question:
fn main() {
let x = 5;
x = 6;
println!("The value of x is: {}", x);
}
cargo run
has the answer for us:
$ cargo run
Compiling bindings v0.1.0 (file:///projects/bindings)
src/main.rs:4:5: 4:10 error: re-assignment of immutable variable `x` [E0384]
src/main.rs:4 x = 6;
^~~~~
src/main.rs:4:5: 4:10 help: run `rustc --explain E0384` to see a detailed explanation
src/main.rs:2:9: 2:10 note: prior assignment occurs here
src/main.rs:2 let x = 5;
^
The error mentions re-assigment of immutable variable
. That’s right: bindings
are immutable. But they’re only immutable by default. In a pattern, when we’re
creating a new name, we can add mut
in front to make the binding a mutable
one. Here’s an example:
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
Running this, we get:
$ cargo run
Compiling bindings v0.1.0 (file:///projects/bindings)
Running `target/debug/bindings`
The value of x is: 5
The value of x is: 6
We can now change the value that x
binds to. Note that the syntax is not let mut
exactly; it’s using mut
in a pattern. This becomes more obvious with our
()
pattern:
fn main() {
let (mut x, y) = (5, 6);
x = 7;
y = 8;
}
The compiler will complain about this program:
$ cargo build
Compiling bindings v0.1.0 (file:///projects/bindings)
src/main.rs:5:5: 5:10 error: re-assignment of immutable variable `y` [E0384]
src/main.rs:5 y = 8;
^~~~~
src/main.rs:5:5: 5:10 help: run `rustc --explain E0384` to see a detailed explanation
src/main.rs:2:17: 2:18 note: prior assignment occurs here
src/main.rs:2 let (mut x, y) = (5, 6);
^
It’s fine with re-assigning x
, but not y
. The mut
only applies to the
name that follows it, not the whole pattern.
Reassignment, not mutation
There is one subtlety we haven’t covered yet: mut
allows you to mutate the
binding, but not what the binding binds to. In other words:
fn main() {
let mut x = 5;
x = 6;
}
This is not changing the value that x
is bound to, but creating a new value,
6
, and changing the binding to bind to it instead. It’s a subtle but
important difference. Well, for now, it does not make a lot of difference, but
when our programs get more complex, it will. Specifically, passing arguments to
functions will illustrate the difference. We’ll talk about that in the next
section, when we discuss functions.
Scope
Variable bindings have a ‘scope’ in which they’re valid. That scope begins from the point at which the binding is declared, and ends at the end of the next block of code. We can only access bindings which are ‘in scope’. We cannot access them ‘before they come into scope’ or ‘after they go out of scope’. Here’s an example:
fn main() {
println!("x is not yet in scope");
let x = 5;
println!("x is now in scope");
println!("In real code, we’d now do a bunch of work.");
println!("x will go out of scope now! The next curly brace is ending the main function.");
}
We can create arbitrary scopes through the use of {
and }
:
fn main() {
println!("x is not yet in scope");
let x = 5;
println!("x is now in scope");
println!("Let’s start a new scope!");
{
let y = 5;
println!("y is now in scope");
println!("x is also still in scope");
println!("y will go out of scope now!");
println!("The next curly brace is ending the scope we started.");
}
println!("x is still in scope, but y is now out of scope and is not usable");
println!("x will go out of scope now! The next curly brace is ending the main function.");
}
What bindings are in and out of scope will become much more important later, once we learn about ‘references’ and ‘traits’.
Shadowing
A final thing about bindings: they can ‘shadow’ previous bindings with the same name. Here’s a sample program:
fn main() {
let x = 5;
let x = 6;
println!("The value of x is: {}", x);
}
Running it, we can see the shadowing in action:
src/main.rs:2:9: 2:10 warning: unused variable: `x`, #[warn(unused_variables)] on by default
src/main.rs:2 let x = 5;
^
Running `target/debug/bindings`
The value of x is: 6
There are two interesting things in this output. First, Rust will compile and
run this program, no problem. And as we can see, the value of x
is 6
. But
we didn’t declare x
as mutable. Instead, we declared a new binding that is
also named x
, and gave it a new value. The older value that we bound x
to
is inaccessible as soon as the new x
is declared. This can be useful if you’d
like to perform a few transformations on a value, and leave it immutable. For
example:
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x);
}
This will print:
Compiling bindings v0.1.0 (file:///projects/bindings)
Running `target/debug/bindings`
The value of x is: 12
This lets us modify x
, but not deal with mutation. This is nice because we
know that the compiler will let us know if we try to modify it later. Let’s
assume that after we calculate 12
, we don’t want to modify x
again. If we
had written this program in a mutable style, like this:
fn main() {
let mut x = 5;
x = x + 1;
x = x * 2;
println!("The value of x is: {}", x);
x = 15;
println!("The value of x is: {}", x);
}
Rust is happy to let us mutate it again, to 15
. A similar program in our
immutable style will let us know about that accidental mutation, however:
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x);
x = 15;
println!("The value of x is: {}", x);
}
If we try to compile, we get an error:
$ cargo build
Compiling bindings v0.1.0 (file:///projects/bindings)
src/main.rs:8:5: 8:11 error: re-assignment of immutable variable `x` [E0384]
src/main.rs:8 x = 15;
^~~~~~
src/main.rs:8:5: 8:11 help: run `rustc --explain E0384` to see a detailed explanation
src/main.rs:4:9: 4:10 note: prior assignment occurs here
src/main.rs:4 let x = x * 2;
^
error: aborting due to previous error
Could not compile `bindings`.
Exactly what we wanted.
Shadowing can take some time to get used to, but it’s very powerful, and works well with immutability.
There was one more thing we should talk about in the output from compiling our initial program. It’s this part:
src/main.rs:2:9: 2:10 warning: unused variable: `x`, #[warn(unused_variables)] on by default
Here’s the two lines of relevant code:
let x = 5;
let x = 6;
Rust knows that we shadowed x
, but we never ended up using the initial value.
This isn’t wrong, exactly, it just may not have been what we wanted. In this
case, the compiler issues a ‘warning’, but still compiles our program. The
#[warn(unused_variables)]
syntax is called an ‘attribute’, which we’ll
discuss in a later section. More specifically, a warning like this is called a
‘lint’, which is an old term for the bits of sheep’s wool that you wouldn’t
want to put in cloth. Similarly, this lint is telling us that we may have an
extra bit of code we don’t need. Our program would work just fine without it.
It’s worth listening to these warnings, and fixing the problems they point out.
They can be signs of a larger problem. In this case, we may not have realized
that we were shadowing x
.
Shadowing and scopes
Like any binding, a binding that shadows another binding will go away at the end of a scope. Here’s an example program:
fn main() {
let x = 5;
println!("Before shadowing, x is: {}", x);
{
let x = 6;
println!("Now that x is shadowed, x is: {}", x);
}
println!("After shadowing, x is: {}", x);
}
If we run this example, we can see the shadow appear and disappear:
$ cargo run
Compiling bindings v0.1.0 (file:///projects/bindings)
Running `target/debug/bindings`
Before shadowing, x is: 5
Now that x is shadowed, x is: 6
After shadowing, x is: 5
Functions
Functions are pervasive in Rust code. We’ve already seen the most important
function, main()
, in previous sections of the book:
fn main() {
println!("Hello, world!");
}
We can declare new functions with the fn
keyword:
fn another_function() {
println!("Another function.");
}
Rust code uses snake_case
as a style for function names: all lower case, with
underscores separating words. (It also uses them for variable names, too.) We
can can call any function we’ve defined by using its name and some parentheses:
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
Let’s start a new project to explore functions. Open a terminal, and navigate to the directory where you’d like to keep your projects. From there, use Cargo to generate a new project:
$ cargo new --bin functions
$ cd functions
Place the new example in src/main.rs
, and run it:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Running `target/debug/functions`
Hello, world!
Another function.
As we can see, the lines execute in order: first, we print out our “Hello,
world!” message, and then another_function()
is called. It then prints its
message as well.
Function Arguments
Functions can also take arguments:
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {}", x);
}
Let’s try running it:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Running `target/debug/functions`
The value of x is: 5
Let’s take a closer look at another_function()
’s signature:
fn another_function(x: i32) {
Declaring a function which takes a single argument looks like this:
fn NAME(PATTERN: TYPE) {
That’s right, patterns appear again. Consider how the parameter declaration
here looks like the let
bindings we used earlier:
let x: i32;
fn another_function(x: i32) {
There’s only one difference here: in function signatures, we must declare the type. This is a deliberate decision; we find that requiring type annotations in functions means that you almost never need them anywhere else.
You can separate multiple arguments with a comma:
fn NAME(PATTERN, PATTERN, PATTERN, PATTERN...) {
Here’s a full example:
fn main() {
another_function(5, 6);
}
fn another_function(x: i32, y: i32) {
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
Let’s try it:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Running `target/debug/functions`
The value of x is: 5
The value of y is: 6
We could also create bindings, and pass them in as arguments:
fn main() {
let a = 5;
let b = 6;
another_function(a, b);
}
fn another_function(x: i32, y: i32) {
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
This has the same effect:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Running `target/debug/functions`
The value of x is: 5
The value of y is: 6
Note that our bindings are called a
and b
, yet inside of the function, we
refer to them by the names in the signature, x
and y
. Inside a function,
only its parameters are in scope, so we need to use those names. Bindings
passed as parameters don’t need to have the same name as the arguments.
Return values
Functions can also return values back to the function that called them:
fn NAME(PATTERN, PATTERN, PATTERN, PATTERN...) -> TYPE {
We don’t name return values, but we do declare their type, after an arrow:
->
. Here’s a sample program:
fn main() {
let x = five();
println!("The value of x is: {}", x);
}
fn five() -> i32 {
5
}
Let’s try running it:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Running `target/debug/functions`
The value of x is: 5
Let’s examine this in more detail. There are two important bits. First, we can use the return value of a function to initialize a binding:
let x = five();
Because five()
returns a 5
, this is the same as:
let x = 5;
The second interesting bit is five()
itself:
fn five() -> i32 {
5
}
We have no arguments, and our return type, i32
. However, the body of this
function is a lonely 5
. There’s a detail here that you may or may not have
noticed: we’ve ended almost every line in our programs with a semicolon.
There’s no semicolon here, though. Why not?
The answer to this question is:
The return value of a function is the value of its final expression.
We haven’t talked about expressions yet, so this definition doesn’t help a lot. Let’s go over that now.
Statements and Expressions
Expressions are bits of code that evaluate to a value. Consider some math operations, like this:
5 + 6
We can evaluate this expression, and come up with a value: 11
. In Rust, most
bits of code are expressions. For example, calling a function is an expression:
foo(5)
The value is equal to whatever the return value of foo()
is.
So why does this matter? Well, not everything is an expression. Some things are
‘statements’. Expressions compute something, but statements bind or do
something. It’s a subtle difference. We’ve already seen two kinds of
statements: let
statements, and fn
declarations.
Because let
is a statement, not an expression, you can’t assign it to another
binding. Here’s an example that doesn’t work:
fn main() {
let x = (let y = 6);
}
If we try to run this program, we’ll get an error:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
src/main.rs:2:14: 2:17 error: expected identifier, found keyword `let`
src/main.rs:2 let x = (let y = 6);
^~~
src/main.rs:2:18: 2:19 error: expected one of `!`, `)`, `,`, `.`, `::`, `{`, or an operator, found `y`
src/main.rs:2 let x = (let y = 6);
^
Could not compile `functions`.
We also cannot somehow assign a fn
declaration to a binding, either.
So what’s this have to do with return values? Well, {}
, a ‘block’ that we
used earlier to create new scopes, is an expression. Let’s take a closer look
at {}
. It looks like this:
{
STATEMENT*
EXPRESSION
}
The *
there means ‘zero or more’, so we can have any number of statements
followed by an expression. Since blocks are expressions themselves, we can nest
blocks inside of blocks. And since they return a value, we can use them in
let
statements:
fn main() {
let x = 5;
let y = {
let z = 1;
x + z + 5
};
println!("The value of y is: {}", y);
}
Let’s try running this program:
Compiling functions v0.1.0 (file:///projects/functions)
Running `target/debug/functions`
The value of y is: 11
We’re now using a block to give us a value for y
:
let y = {
};
Since the block can contain statements, we create a new variable binding, z
,
and give it a value. We then do some math for the final expression of the
block:
{
let z = 1;
x + z + 5
}
5 + 1 + 5
is 11
, and so the value of the entire block is 11
. This gets
substituted into our let
statement for y
:
let y = 11;
Hence our output saying y
is 11
.
Where else do we use blocks? As the body of functions! They’re very similar:
fn main() {
let x = 5;
let y = {
x + 1
};
println!("The value of y is: {}", y);
let y = plus_one(x);
println!("The value of y is: {}", y);
}
fn plus_one(x: i32) -> i32 {
x + 1
}
Running this gives:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Running `target/debug/functions`
The value of y is: 6
The value of y is: 6
In both cases, we use a block to produce a value. In the first case, it’s
assigning with let
:
let y = {
In the second, it’s the return value of the function:
fn plus_one(x: i32) -> i32 {
Expression statements
There’s one more detail about expressions and statements: a semicolon takes any
expression, and turns it into a statement. Let’s accidentally cause an error
with plus_one()
:
fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
Instead of an expression, x + 1
, we’ve now turned it into a statement,
x + 1;
.
Running this gives an error:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
src/main.rs:7:1: 9:2 error: not all control paths return a value [E0269]
src/main.rs:7 fn plus_one(x: i32) -> i32 {
src/main.rs:8 x + 1;
src/main.rs:9 }
src/main.rs:7:1: 9:2 help: run `rustc --explain E0269` to see a detailed explanation
src/main.rs:8:10: 8:11 help: consider removing this semicolon:
src/main.rs:8 x + 1;
^
error: aborting due to previous error
Could not compile `functions`.
Rust has our back here: it even suggests removing the semicolon, which fixes
the error. But the main error message is the core of the issue: statements
don’t evaluate to a value, yet we want to return an i32
.
In practice, Rust programmers don’t often think about these rules at this level. Usually, you have a semicolon at the end of most lines, and maybe not at the end of blocks.
Multiple return values
Functions cannot directly return multiple values. There’s a trick, however.
Remember the ()
s we used when showing off complex bindings?
fn main() {
let (x, y) = (5, 6);
}
They form something called a ‘tuple’, one of Rust’s basic types. A tuple is an anonymous collection of elements. But since a tuple is a singular thing, we can use it as a way to return multiple values from functions:
fn main() {
let (x, y) = two_numbers();
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
fn two_numbers() -> (i32, i32) {
(5, 6)
}
Running this will show us the values:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Running `target/debug/functions`
The value of x is: 5
The value of y is: 6
There are two interesting changes here: assigning the return value of
two_numbers()
to x
and y
, and the declaration of two_numbers()
itself.
Let’s look at the declaration first:
fn two_numbers() -> (i32, i32) {
(5, 6)
}
The (i32, i32)
should look familiar. We saw it in let
bindings earlier:
let (x, y): (i32, i32) = (5, 6);
The (i32, i32)
syntax says “a tuple with two i32
s in it.” The (5, 6)
syntax creates a new one, with 5
and 6
.
This tuple is then returned, and assigned to x
and y
:
let (x, y) = two_numbers();
See how all these bits fit together?
We call this behavior of let
‘destructuring’, because it takes the structure
of the expression that comes after the =
and takes it apart.
Scalar Types
We’ve seen that every value in Rust has a type of some kind. There are a number of types which are built into the language itself. First, we’ll take a look at ‘scalar’ types, that is, types which represent a single value.
Remember, you can rely on type inference to figure out the type of a binding, or you can annotate it explicitly:
fn main() {
let x: i32 = 5;
}
Integers
You’ve already seen one primitive type: i32
. There are a number of built-in
number types in Rust.
Here’s a chart of Rust’s integer types:
signed | unsigned | |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
arch | isize | usize |
We have both signed and unsigned variants of numbers, and each variant has an explicit size. Unsigned numbers are never negative, and signed numbers can be positive or negative. (Think ‘plus sign’ or ‘minus sign’: that’s a signed number.) Signed numbers are stored using ‘two’s complement’ representation.
Finally, isize
and usize
are different sizes based on the kind of computer
your program is running on. If you are on a 64-bit architecture, they are 64
bits, and if you’re on a 32-bit one, they’re 32 bits.
So how do you choose from all these options? Well, if you really don’t know,
the defaults are a good choice: integer types default to i32
. The primary use
case for isize
/usize
is when indexing some sort of collection. We’ll talk
more about our first collection, arrays, in just a moment.
Floating-point numbers
Rust also has two primitive floating-point numbers: f32
and f64
. They are
32 bits and 64 bits in size, respectively. The default is f64
.
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
Floating-point numbers are represented according to the IEEE-754 standard.
f32
is a single-precision float, f64
is double-precision.
Numeric operations
Rust supports the usual operations you’d expect on all of these number types:
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;
// modulus
let remainder = 43 % 5;
}
Booleans
Somewhat fundamental to all computing, Rust has a boolean type, bool
, with
two possible values:
fn main() {
let t = true;
let f: bool = false; // with explict type annotation
}
The main way to consume boolean values is through conditionals like if
, which
we’ll see later in the chapter.
Characters
We’ve only worked with numbers so far, but what about letters? Rust’s most
primitive alphabetic type is the char
:
fn main() {
let c = 'z';
let z = 'ℤ';
}
Rust’s char
represents a Unicode Scalar Value, which means that it can
represent a lot more than just ASCII. ‘Character’ isn’t really a concept in
Unicode, however: your human intutition for what a ‘character’ is may not match
up with a char
. It also means that char
s are four bytes each.
Compound Types
Now that we’ve discussed scalar types, let’s talk about compound types. These types can group multiple values of scalar types into another type.
Tuples
We’ve seen tuples before, in the guise of binding or returning multiple values at once. It turns out that there’s no magic here: tuples are a general way of making a compound value that groups some number of other values with distinct types. The number of values grouped is the ‘arity’ of the tuple.
We create a tuple by writing a comma-separated list of values inside parentheses; each position in the tuple has a distinct type:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
Note that, unlike the examples of multiple bindings, here we bound the
single name tup
to the entire tuple. We can then use pattern
matching to destructure this tuple value:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {}", y);
}
Tuples are used sparingly in Rust code. This is because the elements of a tuple are anonymous, which can make code hard to read.
Tuple indexing
In addition to destructuring through pattern matching, we can also access a
tuple element directly using .
, followed by the index we want to access:
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
As you can see, the first index is 0
.
Single-element tuples
There’s one last trick with tuples: (5)
is actually ambiguous: is it a tuple,
or is it a 5
in parethesis? If you need to disambiguate, use a comma:
fn main() {
let x = (5); // x is an i32, no tuple. Think of it like (5 + 1) without the + 1, they’re for grouping.
let x = (5,); // x is a (i32), a tuple with one element.
}
Arrays
So far, we’ve only represented single values in a binding. Sometimes, though, it’s useful to have more than one value. These kinds of data structures are called ‘collections’, and arrays are the ones we’ll learn about first. Arrays look like this:
fn main() {
let a = [1, 2, 3, 4, 5];
}
An array’s type consists of the type of the elements it contains, as well as the length:
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}
An array is a single chunk of memory, allocated on the stack.
We can access elements of an array using indexing:
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
In this example, first
will hold the value 1
, and second
will be bound to
2
. Note that these values are copied out of the array; if the array changes,
these bindings will not. Here’s an example, which also shows us how we can
modify elements of the array:
fn main() {
let mut a = [1, 2, 3, 4, 5];
let first = a[0];
a[0] = 7;
println!("The value of first is: {}", first);
}
Running this example will show that first
is still 1
. If we didn’t want a
copy, but instead wanted to refer to the first element, whatever its value was,
we need a new concept. We’ll talk about ‘references’ in Section 4.
One last thing: now that we are modifying the array, a
needs to be declared
mut
.
Arrays are our first real data structure, and so there’s a few other concepts
that we haven’t covered in full yet. There are two: the panic!
macro, and a
new way of printing things: Debug
.
Panic
We showed what happens when you access elements of an array, but what if we give an invalid index?
fn main() {
let a = [1, 2, 3, 4, 5];
let invalid = a[10];
println!("The value of invalid is: {}", invalid);
}
If we run this example, we will get an error. Let’s re-use our functions
project from before. Change your src/main.rs
to look like the example, and
run it:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Running `target/debug/functions`
thread ‘<main>’ panicked at ‘index out of bounds: the len is 5 but the index is 10’, src/main.rs:4
Process didn’t exit successfully: `target/debug/functions` (exit code: 101)
It says that our thread panicked, and that our program didn’t exit successfully. There’s also a reason: we had a length of five, but an index of 10.
For now, all you need to know is that a panic will crash your program. Rust’s error handling story is described in full in a later chapter.
So why did this code panic? Well, arrays know how many elements they hold. When we access an element via indexing, Rust will check that the index is less than the length. If it’s greater, it will panic, as something is very wrong. This is our first example of Rust’s safety principles in action. In many low-level languages, this kind of check is not done. If you have an incorrect index, invalid memory can be accessed. Rust protects us against this kind of error.
Debug
So far, we’ve been printing values using {}
. If we try that with an array,
though...
fn main() {
let a = [1, 2, 3, 4, 5];
println!("a is: {}", a);
}
... we will get an error:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
src/main.rs:4:25: 4:26 error: the trait `core::fmt::Display` is not implemented for the type `[_; 5]` [E0277]
src/main.rs:4 println!(“a is {}”, a);
^
<std macros>:2:25: 2:56 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
src/main.rs:4:5: 4:28 note: in this expansion of println! (defined in <std macros>)
src/main.rs:4:25: 4:26 help: run `rustc --explain E0277` to see a detailed explanation
src/main.rs:4:25: 4:26 note: `[_; 5]` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string
src/main.rs:4:25: 4:26 note: required by `core::fmt::Display::fmt`
error: aborting due to previous error
Whew! The core of the error is this part: the trait core::fmt::Display
is not
implemented. We haven’t discussed traits yet, so this is bound to be confusing!
Here’s all we need to know for now: println!
can do many kinds of formatting.
By default, {}
implements a kind of formatting known as Display
: output
intended for direct end-user consumption. The primitive types we’ve seen so far
implement Display
, as there’s only one way you’d show a 1
to a user. But
with arrays, the output is less clear. Do you want commas or not? What about
the []
s?
Due to these questions, more complex types in the standard library do not
implement Display
formatting. There is another kind of formatting, Debug
,
which is a bit different: intended for programmer consumption. We can ask
println!
to use Debug
formatting with :?
:
fn main() {
let a = [1, 2, 3, 4, 5];
println!("a is {:?}", a);
}
This will work:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Running `target/debug/functions`
a is [1, 2, 3, 4, 5]
You’ll see this repeated later, with other types. And we’ll cover traits fully later in the book, Section 9.
Comments
We strive to make our programs easy to understand, but sometimes, some extra explanation is warranted. We can leave notes in our source code that the compiler will ignore. These notes are called ‘comments’.
Here’s a comment:
// Hello, world.
Comments start with two slashes, and last until the end of the line. Larger comments will need more lines:
// 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 go at the end of lines:
fn main() {
let lucky_number = 7; // I’m feeling lucky today.
}
But you’ll more often see them above:
fn main() {
// I’m feeling lucky today.
let lucky_number = 7;
}
That’s all there is to it. Comments are not particularly complicated.
Documentation comments
However, Rust has another kind of comment: a documentation comment. These
comments don’t affect the way that the code works, but they do work with Rust’s
tools. More specifically, the rustdoc
tool that comes with Rust reads
documentation comments and produces HTML documentation from them.
Documentation comments use an extra slash:
/// The foo function doesn’t really do much.
fn foo() {
}
/// We also can use
/// multiple comments here too,
/// like we did before
fn bar() {
}
This comment would then be interpreted by rustdoc
as documenting the thing
that follows it: foo()
and bar()
.
Because documentation comments have semantic meaning to rustdoc
, the compiler
will pay attention to the placement of your documentation comments. For
example, a program with only this:
/// What am I documenting?
Will give a compiler error:
src/main.rs:1:1: 1:27 error: expected item after doc comment
src/main.rs:1 /// What am I documenting?
^~~~~~~~~~~~~~~~~~~~~~~~~~
Control flow with if
Two roads diverged in a yellow wood, And sorry I could not travel both And be one traveler, long I stood And looked down one as far as I could To where it bent in the undergrowth;
- Robert Frost, “The Road Not Taken”
In Rust, there are a few ways to cause our code to branch. The most fundamental
way is by using if
. An if
expression gives us two paths forward, and asks
the question, “Which one should I take?”
Let’s make a new project to explore if
. Navigate to your projects directory,
and use Cargo to make a new project called branches
:
$ cargo new --bin branches
$ cd branches
Here’s a sample program using if
:
fn main() {
let condition = true;
if condition {
println!("condition was true");
} else {
println!("condition was false");
}
}
Let's try running it:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Running `target/debug/branches`
condition was true
We can change the value of condition
:
let condition = false;
And then run it again:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Running `target/debug/branches`
condition was false
This is the very basic structure of if
: if the condition is true, then
execute some code. If it’s not true, then execute some other code, after
else
.
An else
is not required:
fn main() {
let condition = false;
if condition {
println!("condition was true");
}
}
In this case, nothing is printed.
It’s also worth noting that condition
here must be a bool
. Let’s try an
example with something else:
fn main() {
let condition = 5;
if condition {
println!("condition was five");
}
}
If we try to run this program, Rust will complain:
Compiling branches v0.1.0 (file:///projects/branches)
src/main.rs:4:8: 4:17 error: mismatched types:
expected `bool`,
found `_`
(expected bool,
found integral variable) [E0308]
src/main.rs:4 if condition {
^~~~~~~~~
src/main.rs:4:8: 4:17 help: run `rustc --explain E0308` to see a detailed explanation
error: aborting due to previous error
Could not compile `branches`.
We expected a bool
, but got an integer. Rust will not automatically try to convert non-boolean types to a boolean here. We must be explicit.
else if
We can make multiple decisions by combining if
and else
in another way:
fn main() {
let number = 5;
if number == 3 {
println!("condition was 3");
} else if number == 4 {
println!("condition was 4");
} else if number == 5 {
println!("condition was 5");
} else {
println!("condition was something else");
}
}
Let's try running it:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Running `target/debug/branches`
condition was 5
When this program executes, it will check each if
in turn, and execute the
first body for which the condition holds true.
Using a single else if
can be okay, but if you find yourself with more than one,
you may want to refactor your code. Rust has a more powerful branching construct
called match
for these cases. We'll cover it later, when we talk about enums
.
if
as an expression
There’s one last detail we need to learn about if
: it’s an expression. That means
that we can use it on the right hand side of a let
binding, for instance:
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
println!("The value of number is: {}", number);
}
Let’s run this:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Running `target/debug/branches`
The value of number is: 5
Remember, blocks of code evaluate to the last expression in them. And numbers
by themselves are also expressions. So in this case, the value of the whole
if
expression depends on which block of code executes.
There’s another small detail involved here: this means that if you use if
in this way, both arms of the if
must be the same type. This doesn’t work:
fn main() {
let condition = true;
let number = if condition {
5
} else {
"six"
};
println!("The value of number is: {}", number);
}
If we try to run this, we’ll get an error:
Compiling branches v0.1.0 (file:///projects/branches)
src/main.rs:4:18: 8:6 error: if and else have incompatible types:
expected `_`,
found `&‘static str`
(expected integral variable,
found &-ptr) [E0308]
src/main.rs:4 let number = if condition {
src/main.rs:5 5
src/main.rs:6 } else {
src/main.rs:7 "six"
src/main.rs:8 };
src/main.rs:4:18: 8:6 help: run `rustc --explain E0308` to see a detailed explanation
error: aborting due to previous error
Could not compile `branches`.
if
and else
have incompatible types. This can’t work.
Loops
It’s often quite useful to be able to execute a block of code more than one time. For this, we have several constructs, called ‘loops’.
To try out loops, let’s make a new project. Navigate to your projects folder and use Cargo to make a new one:
$ cargo new --bin loops
$ cd loops
There are three kinds of loops in Rust: loop
, while
, and for
. Let’s dig
in.
loop
The loop
keyword is very straightforward: it executes a block of code over
and over and over and over and over and over forever. Change your src/main.rs
file to look like this:
fn main() {
loop {
println!("again!");
}
}
If we run this program, we’ll see ‘again!
’ printed over and over again. So
how does our program end? It doesn’t, until we kill it. Most terminals support
a keyboard shortcut, ‘control-c’, to stop a runaway program. Give it a try:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
That ^C
there is where I hit control-c.
That’s a lot of trouble though! Luckily, there’s a way to break an infinite loop
.
Breaking out of a loop
The break
keyword will allow us to quit looping. Try this version out:
fn main() {
loop {
println!("once!");
break;
}
}
If you run this program with cargo run
, you’ll see that it only executes one
time:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Running `target/debug/loops`
once!
When a Rust program hits a break
statement, it will exit the current loop.
while
What if we took loop
, break
, and if
, and put them together? Something
like this:
fn main() {
let mut number = 3;
loop {
if number != 0 {
println!("{}!", number);
number = number - 1;
} else {
break;
}
}
println!("LIFTOFF!!!");
}
If we run this, we’ll get some output:
Compiling loops v0.1.0 (file:///projects/loops)
Running `target/debug/loops`
3!
2!
1!
LIFTOFF!!!
The core of this example is in the combination of these three constructs:
loop {
if number != 0 {
// do stuff
} else {
break;
}
We want to loop
, but only while some sort of condition is true. As soon as it
isn't, we want to break
out of the loop.
This pattern is so common that we have a language construct for it: while
.
Here's the same example, but using while
instead:
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
println!("LIFTOFF!!!");
}
This lets us get rid of a lot of nesting, and is more clear: while a condition holds, run this code.
for
We can use this while
construct to loop over the elements of a collection, like an
array:
fn main() {
let a = [1, 2, 3, 4, 5];
let mut index = 0;
while index < 5 {
println!("the value is is: {}", a[index]);
index = index + 1;
}
}
Running this will print out every element of the array:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Running `target/debug/loops`
the value is: 1
the value is: 2
the value is: 3
the value is: 4
the value is: 5
Here, we're counting up instead of down: we start at zero, then loop until we hit the final index of our array.
This approach is error-prone, though. If we get the index length incorrect, we
will end up causing a panic!
. This is also slow, as the compiler needs to do
that check on every element on every iteration through the loop.
Instead, we can use our last kind of loop: the for
loop. It looks like this:
fn main() {
let a = [1, 2, 3, 4, 5];
let mut index = 0;
for element in a.iter() {
println!("the value is: {}", element);
}
}
** NOTE: see https://github.com/rust-lang/rust/issues/25725#issuecomment-166365658, we may want to change this **
If we run this, we'll see the same output as the previous example.
** I'm going to leave it at this for now until we decide how we want to do it**