rust-book-cn/nostarch/chapter3.md
2016-01-14 10:53:47 +06:00

50 KiB
Raw Blame History

Up and Running

Well 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 well cover Rusts 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. Lets start with a short example.

The basics of bindings

First, well generate a new project with Cargo. Open a terminal, and navigate to the directory where youd like to keep your projects. From there, lets 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!");
}

Lets 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. Lets break this down, line by line.

fn main() {

The main() function is the entry point of every Rust program. Well 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 functions 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, well see more complex and powerful patterns as we go along.

Before we do that, though, lets finish investigating this example. Heres the next line:

    println!("The value of x is: {}", x);

The println! macro prints text to the screen. We can tell that its a macro due to the !. We wont learn how to write macros until much later in the book, but well 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. Heres 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 well 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

Lets 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

Weve created two bindings with one let! Heres our pattern:

(x, y)

And heres 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 lets may be clearer, but in others, creating multiple bindings at once is nice. As we become more proficient in Rust, well figure out which style is better, but its mostly a judgement call.

Type annotations

You may have noticed that we didnt 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. Lets 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. Well talk more about Rusts basic types in section 3.3.

Heres 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. Heres 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, heres 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

Its all good. This raises a question, though: what if we try to print out a binding before we declare a value? Heres 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 wont 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

Theres 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. Heres 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 youre stuck on an error. The compiler is your friend, and is here to help.

Mutable bindings

What about changing the value of a binding? Heres 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. Thats right: bindings are immutable. But theyre only immutable by default. In a pattern, when were creating a new name, we can add mut in front to make the binding a mutable one. Heres 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; its 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);
                              ^

Its 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 havent 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. Its 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. Well talk about that in the next section, when we discuss functions.

Scope

Variable bindings have a scope in which theyre 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. Heres an example:

fn main() {
    println!("x is not yet in scope");

    let x = 5;
    println!("x is now in scope");

    println!("In real code, wed 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!("Lets 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. Heres 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 didnt 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 youd 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. Lets assume that after we calculate 12, we dont 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 its very powerful, and works well with immutability.

There was one more thing we should talk about in the output from compiling our initial program. Its this part:

src/main.rs:2:9: 2:10 warning: unused variable: `x`, #[warn(unused_variables)] on by default

Heres 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 isnt 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 well discuss in a later section. More specifically, a warning like this is called a lint, which is an old term for the bits of sheeps wool that you wouldnt want to put in cloth. Similarly, this lint is telling us that we may have an extra bit of code we dont need. Our program would work just fine without it. Its 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. Heres 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. Weve 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 weve defined by using its name and some parentheses:

fn main() {
    println!("Hello, world!");

    another_function();
}

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

Lets start a new project to explore functions. Open a terminal, and navigate to the directory where youd 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);
}

Lets try running it:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
     Running `target/debug/functions`
The value of x is: 5

Lets 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) {

Thats 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) {

Theres 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...) {

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

Lets 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 dont 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 dont name return values, but we do declare their type, after an arrow: ->. Heres a sample program:

fn main() {
    let x = five();

    println!("The value of x is: {}", x);
}

fn five() -> i32 {
    5
}

Lets try running it:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
     Running `target/debug/functions`
The value of x is: 5

Lets 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. Theres a detail here that you may or may not have noticed: weve ended almost every line in our programs with a semicolon. Theres 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 havent talked about expressions yet, so this definition doesnt help a lot. Lets 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. Its a subtle difference. Weve already seen two kinds of statements: let statements, and fn declarations.

Because let is a statement, not an expression, you cant assign it to another binding. Heres an example that doesnt work:

fn main() {
    let x = (let y = 6);
}

If we try to run this program, well 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 whats this have to do with return values? Well, {}, a block that we used earlier to create new scopes, is an expression. Lets 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);
}

Lets try running this program:

   Compiling functions v0.1.0 (file:///projects/functions)
     Running `target/debug/functions`
The value of y is: 11

Were 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! Theyre 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, its assigning with let:

let y = {

In the second, its the return value of the function:

fn plus_one(x: i32) -> i32 {

Expression statements

Theres one more detail about expressions and statements: a semicolon takes any expression, and turns it into a statement. Lets 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, weve 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 dont evaluate to a value, yet we want to return an i32.

In practice, Rust programmers dont 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. Theres 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 Rusts 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.

Lets 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 i32s 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

Weve 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, well 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

Youve already seen one primitive type: i32. There are a number of built-in number types in Rust.

Heres a chart of Rusts 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: thats a signed number.) Signed numbers are stored using twos 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 youre on a 32-bit one, theyre 32 bits.

So how do you choose from all these options? Well, if you really dont 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. Well 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 youd 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 well see later in the chapter.

Characters

Weve only worked with numbers so far, but what about letters? Rusts most primitive alphabetic type is the char:

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

Rusts char represents a Unicode Scalar Value, which means that it can represent a lot more than just ASCII. Character isnt 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 chars are four bytes each.

Compound Types

Now that weve discussed scalar types, lets talk about compound types. These types can group multiple values of scalar types into another type.

Tuples

Weve seen tuples before, in the guise of binding or returning multiple values at once. It turns out that theres 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

Theres 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, theyre for grouping.

    let x = (5,); // x is a (i32), a tuple with one element.
}

Arrays

So far, weve only represented single values in a binding. Sometimes, though, its useful to have more than one value. These kinds of data structures are called collections, and arrays are the ones well learn about first. Arrays look like this:

fn main() {
    let a = [1, 2, 3, 4, 5];
}

An arrays 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. Heres 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 didnt want a copy, but instead wanted to refer to the first element, whatever its value was, we need a new concept. Well 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 theres a few other concepts that we havent 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. Lets 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 didnt exit successfully: `target/debug/functions` (exit code: 101)

It says that our thread panicked, and that our program didnt exit successfully. Theres 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. Rusts 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 its greater, it will panic, as something is very wrong. This is our first example of Rusts 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, weve 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 havent discussed traits yet, so this is bound to be confusing! Heres 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 weve seen so far implement Display, as theres only one way youd 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]

Youll see this repeated later, with other types. And well 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.

Heres a comment:

// Hello, world.

Comments start with two slashes, and last until the end of the line. Larger comments will need more lines:

// So were doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain whats going on.

Comments can also go at the end of lines:

fn main() {
    let lucky_number = 7; // Im feeling lucky today.
}

But youll more often see them above:

fn main() {
    // Im feeling lucky today.
    let lucky_number = 7;
}

Thats all there is to it. Comments are not particularly complicated.

Documentation comments

However, Rust has another kind of comment: a documentation comment. These comments dont affect the way that the code works, but they do work with Rusts 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 doesnt 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?”

Lets 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

Heres 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 its 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.

Its also worth noting that condition here must be a bool. Lets 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

Theres one last detail we need to learn about if: its 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);
}

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

Theres 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 doesnt 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, well 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 cant work.

Loops

Its 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, lets 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. Lets 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, well see again! printed over and over again. So how does our program end? It doesnt, 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.

Thats a lot of trouble though! Luckily, theres 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, youll 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, well 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**