We’re now moving past Rust’s simplest types to collection types. Rust has a lot of collection types, and in this chapter, we’ll learn three of them: arrays, vectors, and tuples. Unsurprisingly, Rust gives you a lot of options to choose from; this chapter only shows a few. After collection types, we’ll learn about control flow, which means telling Rust how to run your code depending on the situation. One of the coolest parts of control flow in Rust is the keyword match
, so keep an eye out for that.
Rust has a lot of types for making collections. Collections are used when you have more than one value and want to hold them in a single place with some sort of order. For example, you could have information on all the cities in your country inside one collection.
The collection types we are going to look at now are arrays, vectors, and tuples. These are the easiest collection types in Rust to learn. There are other, more complex collection types in Rust, but those won’t come up until chapter 6! We will start with arrays. Arrays are simpler types than vectors, so they can be used in places like tiny embedded devices where you can’t allocate memory. At the same time, they have the least functionality for the user. They are a little bit like &str
in that way.
To create an array, just put some data inside square brackets separated by commas. But arrays have some pretty strict rules:
Arrays have a somewhat interesting type: [type; number]
. For example, the type of ["One", "Two"]
is [&str; 2]
, while the type of ["One"]
is [&str; 1]
. This means that even these two arrays are of different types:
fn main() { let array1 = ["One", "Two"]; ① let array2 = ["One", "Two", "Five"]; ② }
② But this one is type [&str; 3]. Different type!
Here is a good tip for arrays as well as other types: to find the type of a variable, you can “ask” the compiler by giving it bad instructions, such as trying to call a method that doesn’t exist. Take this code for example:
fn main() { let seasons = ["Spring", "Summer", "Autumn", "Winter"]; let seasons2 = ["Spring", "Summer", "Fall", "Autumn", "Winter"]; seasons.ddd(); ① seasons2.thd(); ② }
The compiler says, “What? There’s no .ddd()
method for seasons and no .thd()
method for seasons 2 either!!” as the error output shows us:
error[E0599]: no method named `ddd` found for array `[&str; 4]` in the current scope
--> src\main.rs:4:13
|
4 | seasons.ddd();
| ^^^ method not found in `[&str; 4]`
error[E0599]: no method named `thd` found for array `[&str; 5]`
➥in the current scope
--> src\main.rs:5:14
|
5 | seasons2.thd();
| ^^^ method not found in `[&str; 5]`
So when the compiler tells you method not found in `[&str; 4]`
, that’s the type.
If you want an array with all the same value, you can declare it by entering the value, then a semicolon, and then the number of times you need it to repeat:
fn main() { let my_array = ["a"; 5]; println!("{:?}", my_array); }
This prints ["a", "a", "a", "a", "a"]
.
This method is used a lot to create byte buffers, which computers use when doing operations like downloading data. For example, let mut buffer = [0u8; 640]
creates an array of 640 u8
zeroes, which means 640 bytes of empty data. Its type will then be [u8; 640]
. When data comes in, it can change each zero to a different u8
number to represent the data. This buffer can change up to 640 of these zeroes before it is “full.” We won’t try to do any of these operations in Rust in this chapter, but it’s good to know what arrays can be used for.
As you can see, you can change the data inside an array as much as you want (if it’s mut
, of course). You just can’t add or remove items or change the type of the items inside.
We can use the b
prefix that we learned in the previous chapter to take a look at an array of bytes. This example won’t compile yet, but the error message is interesting:
fn main() { println!("{}", b"Hello there"); }
error[E0277]: `[u8; 11]` doesn't implement `std::fmt::Display`
--> src/main.rs:2:20
|
2 | println!("{}", b"Hello there");
| ^^^^^^^^^^^^^^ `
➥[u8; 11]` cannot be formatted with the default formatter
|
The solution is to use {:?}
instead of {}
, but we don’t care about that: what’s interesting is the type. It’s [u8; 11]
. So when you use b
, it turns a &str
into a byte array
: an array of u8
.
You can index (get) entries in an array with []
. The first entry is [0]
, the second is [1]
, and so on:
fn main() {
let my_numbers = [0, 10, -20];
println!("{}", my_numbers[1]); ①
}
You can also get a slice (a piece) of an array. First, you need a &
because the compiler doesn’t know the size (a slice can be any length, so it is not Sized
). Then you can use ..
to show the range. A range between index 2 and 5, for example, is 2..5
. But remember, in 2..5, 2
means the third item (because indexes start at 0), and 5
means “up to index 5, but not including it.”
This is easier to understand with examples. Let’s use the array [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
and slice it in different ways:
fn main() { let array_of_ten = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let two_to_five = &array_of_ten[2..5]; ① let start_at_one = &array_of_ten[1..]; ② let end_at_five = &array_of_ten[..5]; ③ let everything = &array_of_ten[..]; ④ println!("Two to five: {two_to_five:?}, Start at one: {start_at_one:?}, End at five: {end_at_five:?}, Everything: {everything:?}"); }
① 2..5 means from index 2 up to index 5 but not including index 5.
② 1.. means from index 1 until the end.
③ ..5 means from the beginning up to but not including index 5.
④ Using .. means to slice the whole array: beginning to end.
Two to five: [2, 3, 4], Start at one: [1, 2, 3, 4, 5, 6, 7, 8, 9], End at five: [0, 1, 2, 3, 4], Everything: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Because a range like 2..5
doesn’t include index 5, it’s called exclusive. But you can also have an inclusive range, which means it includes the last number, too. To do this, add =
to write ..=
instead of the regular ..
two dots. So instead of [0..2]
you can write [0..=2]
if you want the first, second, and third item (these are also called the zeroth, first, and second index).
In addition to arrays, we have vectors. The difference between the two is similar to the difference between &str
and String
: arrays are simpler, with less flexibility and functionality, and may be faster, while vectors are easier to work with because you can change their size. (Note that arrays are not dynamically sized like a &str
, so the compiler always knows their size. That’s why we didn’t need a reference to use them in the previous examples.)
The vector type is written Vec
, and most people simply call it a Vec. It rhymes with deck. There are two main ways to declare a vector. One is similar to making a String
using new
:
fn main() { let name1 = String::from("Windy"); ① let name2 = String::from("Gomesy"); ② let mut my_vec = Vec::new(); ③ my_vec.push(name1); ④ my_vec.push(name2); }
③ If we run the program now, the compiler will give an error. It doesn’t know the type of Vec.
④ Now it knows: it’s a Vec<String>
You can see that Vec
always has something else inside it, and that’s what the <>
(angle brackets) are for. A Vec<String>
is a vector with one or more String
s. You can put anything inside a Vec
—for example:
Vec<(i32, i32)>
—This is a Vec where each item is a tuple: (i32, i32)
. We will learn tuples right after Vecs.
Vec<Vec<String>>
—This is a Vec that has Vec
s of String
s. Say, for example, you wanted to save the words of your favorite book as a Vec<String>
. Then you do it again with another book and get another Vec<String>
. To hold both books, you would put them into another Vec
, and that would be a Vec<Vec<String>>
.
Instead of using .push()
to have Rust decide the type (using type inference), you can declare the type:
fn main() {
let mut my_vec: Vec<String> = Vec::new(); ①
}
① The compiler knows that it is a Vec<String>, so it won’t generate an error.
All items in a Vec must all have the same type, so you can’t push an i32
or anything else into a Vec<String>
.
Another easy way to create a Vec is with the vec!
macro. It looks like an array declaration but has vec!
in front of it. Most people make Vecs this way because it’s so easy:
fn main() { let mut my_vec = vec![8, 10, 10]; }
You can slice a vector, too, just like in an array. The following code is the same as the previous array example, except it uses Vecs instead of arrays:
fn main() { let vec_of_ten = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let three_to_five = &vec_of_ten[2..5]; let start_at_two = &vec_of_ten[1..]; let end_at_five = &vec_of_ten[..5]; let everything = &vec_of_ten[..]; println!("Three to five: {:?}, start at two: {:?} end at five: {:?} everything: {:?}", three_to_five, start_at_two, end_at_five, everything); }
Vecs allocate memory, so they have some methods to reduce memory usage and make them faster. A Vec has a capacity, which means the amount of memory given to the Vec to use. As you push new items onto the Vec, it gets closer and closer to the capacity. It won’t give an error if you go past the capacity, so don’t worry. However, if you go past the capacity, it will double its capacity and copy the items into this new memory space.
For example, imagine that you have a Vec with a capacity of 4 and four items inside it. If you add one more item, it will need a new memory space that can hold all five items. So, it will double its capacity to 8 and copy the five items over into the new memory space. This is called reallocation. You can imagine that this will use extra memory if you keep pushing a lot. We’ll use a method called .capacity()
to look at the capacity of a Vec as we add items to it, as in the following example:
fn main() { let mut num_vec = Vec::new(); println!("{}", num_vec.capacity()); ① num_vec.push('a'); ② println!("{}", num_vec.capacity()); ③ num_vec.push('a'); ④ num_vec.push('a'); ④ num_vec.push('a'); ④ println!("{}", num_vec.capacity()); ⑤ num_vec.push('a'); ④ println!("{}", num_vec.capacity()); ⑥ }
③ One element: prints 4. Vecs with one item always start with capacity 4.
⑤ Four elements: still prints 4
⑥ Prints 8. We have five elements, but it doubled four to eight to make space.
0 4 4 8
This vector has two reallocations: 0 to 4 and 4 to 8. We can make it more efficient by giving it a capacity of 8 to start:
fn main() { let mut num_vec = Vec::with_capacity(8); ① num_vec.push('a'); ② println!("{}", num_vec.capacity()); ③ num_vec.push('a'); ④ println!("{}", num_vec.capacity()); ③ num_vec.push('a'); ④ println!("{}", num_vec.capacity()); ③ num_vec.push('a'); ④ num_vec.push('a'); ⑤ println!("{}", num_vec.capacity()); ⑥ }
⑤ Adds one more. Now we have five elements.
This vector had just a single first allocation, which is much better. If you think you know how many elements you need, you can use Vec::with_capacity()
to use less memory and make your program more efficient.
We saw in the previous chapter that you can use .into()
to make a &str
into a String
. You can also use the same method to make an array into a Vec
. Interestingly, you have to declare that you want to use .into()
to make a Vec
, but you don’t have to say which kind! You can simply write Vec<_>
, and thanks to type inference, Rust will change the array into a Vec
for you:
fn main() {
let my_vec: Vec<u8> = [1, 2, 3].into();
let my_vec2: Vec<_> = [9, 0, 10].into(); ①
}
The last collection type in this chapter is called a tuple, which is quite different because it lets you hold a collection of different types together. Internally, tuples are different, too. Let’s see how.
Tuples in Rust use ()
. We have seen many empty tuples already because nothing in a function means an empty tuple. The signature
fn do_something() {}
fn do_something() -> () {}
That function gets nothing (an empty tuple) and returns nothing (an empty tuple). We have been using tuples a lot already. When you don’t return anything in a function, you return an empty tuple. In Rust, this empty tuple is called the unit type. Take a look at the following example and think about what is being returned in both the previous function and inside main()
:
fn just_makes_an_i32() { let unused_number = 10; } fn main() { just_makes_an_i32() }
In the function just_makes_an_i32()
, we make an i32
that we never use. It gets declared inside the function and is followed with a semicolon. When you end a line with a semicolon nothing is returned—just an empty tuple. So, the return value for this function is also ()
. Then main()
starts, and main()
is also a function that returns nothing—an empty tuple. The interesting part is that just_makes_an_i32()
isn’t followed by a semicolon, but the code still works! That is because just_makes_an_i32()
returns a (),
and that becomes the return value for the main function because just_makes_an_i32()
is on the last line. Of course, it looks much better to write just_makes_an_i32();
with a semicolon. But this is a good lesson to see that the Rust compiler isn’t concerned with whether you use semicolons; it’s a compiler, not a formatter. It is only interested in having the expected inputs and outputs match.
Let’s go beyond empty tuples and look at tuples that hold values. Items inside a tuple are also accessed with numbers 0, 1, 2, and so on. But to access them, you use a .
(dot) instead of a []
. There is a good reason for this: tuples are more like objects than indexed collections. In the next chapter, we will learn how to make objects called structs that use the same . (dot) notation.
Okay, let’s put a whole bunch of types into a single tuple:
fn main() { let random_tuple = ("Here is a name", 8, vec!['a'], 'b', [8, 9, 10], 7.7); println!( "Inside the tuple is: First item: {:?} Second item: {:?} Third item: {:?} Fourth item: {:?} Fifth item: {:?} Sixth item: {:?}", random_tuple.0, random_tuple.1, random_tuple.2, random_tuple.3, random_tuple.4, random_tuple.5, ) }
Inside the tuple is: First item: "Here is a name" Second item: 8 Third item: ['a'] Fourth item: 'b' Fifth item: [8, 9, 10] Sixth item: 7.7
The type of a tuple depends on the types of the items inside it. So this tuple is of type (&str, i32, Vec<char>, char, [i32; 3], f64)
.
You can use a tuple to create multiple variables at the same time. Take a look at this code:
fn main() {
let strings = ("one".to_string(),
➥"two".to_string(), "three".to_string());
}
This strings
tuple has three items in it. What if we want to pull them out and use them separately? We can use another tuple for that:
fn main() {
let strings = ("one".to_string(),
"two".to_string(), "three".to_string());
let (a, b, c) = strings;
println!("{b}");
// println!("{strings:?}"); ①
}
That prints two
, which is the value that b
holds. This is known as destructuring because the variables are first inside a structure, but then we made a, b
, and c
separately to pull this structure apart. A String
is not a Copy type, so the values are moved into a, b
, and c,
and strings
can’t be accessed anymore.
Destructuring only works when the pattern matches. The following code works because each side has three items—the patterns match:
fn main() { let tuple_of_three = ("one", "two", "three"); let (a, b, c) = tuple_of_three; }
But you can’t destructure if the pattern doesn’t match. The next code sample is trying to use a tuple of two items to destructure three items, but the patterns don’t match, and Rust can’t tell what kind of destructuring you are trying to do:
fn main() {
let tuple_of_three = ("one", "two", "three");
let (a, b) = tuple_of_three; ①
}
① Should _b_ be "two" or "three"? Rust can’t tell.
If you write let (a, b, c)
instead of let (a, b)
, then they will match, and you will have the variables a, b
, and c
to use. But what if you only want to use two items? No problem, just make sure the pattern matches but use _
instead of a variable name:
fn main() { let tuple_of_three = ("one", "two", "three"); let (_, b, c) = tuple_of_three; }
Now Rust can tell that you want b
and c
to have the values "two"
and "three"
, and "one"
doesn’t get assigned to any variable.
In chapter 6, we’ll see more collection types, and we’ll see more ways to use them all throughout the book as well. But for the remainder of this chapter, we will learn control flow.
Control flow involves telling your code to do something in a certain situation but to do something else in another situation. What should the code do if a certain condition is true, or a number is even or odd, or some other case? Rust has quite a few ways to manage control flow, and we’ll start with the simplest form: the keyword if
.
The simplest form of control flow is if
followed by {}
. Rust will execute the code inside {}
if the condition is true
and will do nothing otherwise:
fn main() { let my_number = 5; if my_number == 7 { println!("It's seven"); } }
This code will print nothing because my_number
is not 7.
Also note that you use ==
and not =
. Using ==
is to compare, while =
is to assign (to give a value). Also note that we wrote if my_number == 7
and not if (my_number == 7)
. You don’t need parentheses with if
in Rust. Using if
will work with parentheses, but the compiler will tell you that you didn’t need to use them.
You can use else if
and else
to give you more control:
fn main() { let my_number = 5; if my_number == 7 { println!("It's seven"); } else if my_number == 6 { println!("It's six") } else { println!("It's a different number") } }
This prints It's a different number
because my_number
isn’t equal to 7 or 6.
You can add more conditions with &&
(and) and ||
(or):
fn main() {
let my_number = 5;
if my_number % 2 == 1 && my_number > 0 { ①
println!("It's a positive odd number");
} else if my_number == 6 {
println!("It's six")
} else {
println!("It's a different number")
}
}
① This % is called modulo and gives the number that remains after dividing. 9 % 3 would give 0, and 5 % 2 would give 1.
This prints It's a positive odd number
because when you divide it by 2, you have a remainder of 1, and it’s greater than 0.
You can already see that using if, else
, and else if
too much can make your code difficult to read. In this case, you can use match
instead, which looks much cleaner. But Rust will make you match for every possible situation and won’t compile the code otherwise. For example, this will not work:
fn main() { let my_number: u8 = 5; match my_number { 0 => println!("it's zero"), 1 => println!("it's one"), 2 => println!("it's two"), } }
error[E0004]: non-exhaustive patterns: `3u8..=std::u8::MAX` not covered --> src\main.rs:3:11 | 3 | match my_number { | ^^^^^^^^^ pattern `3u8..=std::u8::MAX` not covered
The compiler is saying, “You told me about 0 to 2, but u8
s can go up to 255. What about 3? What about 4? What about 5?” And so on. In this case, you can add _
(underscore), which means “anything else.” This is sometimes called a wildcard:
fn main() { let my_number: u8 = 5; match my_number { 0 => println!("it's zero"), 1 => println!("it's one"), 2 => println!("it's two"), _ => println!("It's some other number"), } }
That prints It's some other number
.
Remember these points for match
:
You write match
, then the name of the item to match against, and then a {}
code block.
Write the pattern on the left and use a =>
(fat arrow) to say what to do when the pattern also matches.
You can declare a value with a match
:
fn main() { let my_number = 5; let second_number = match my_number { 0 => 0, 5 => 10, _ => 2, }; }
The variable second_number
will be 10. Do you see the semicolon at the end? That is because after the match is over, we told the compiler this: let second_number = 10;
.
You can match on more complicated patterns, too. You can use a tuple to do it:
fn main() { let sky = "cloudy"; let temperature = "warm"; match (sky, temperature) { ("cloudy", "cold") => println!("It's dark and unpleasant today"), ("clear", "warm") => println!("It's a nice day"), ("cloudy", "warm") => println!("It's dark but not bad"), _ => println!("Not sure what the weather is."), } }
This prints It's dark but not bad
because it matches "cloudy"
and "warm"
for sky
and temperature
.
You can even put if
inside of match
. This is called a match guard:
fn main() { let children = 5; let married = true; match (children, married) { (children, married) if married == false => println!("Not married with {children} kids"), (children, married) if children == 0 && married == true => { println!("Married but no children") } _ => println!("Married? {married}. Number of children: {children}."), } }
This will print Married? true. Number of children: 5
.
You also don’t need to write ==
true or == false
when checking a bool
. Instead, you can write the name of the variable by itself (to check if true
) or the name of the variable with an exclamation mark in front (to check if false
). Here’s the same code as before using this shortcut:
fn main() { let children = 5; let married = true; match (children, married) { (children, married) if !married => ➥println!("Not married with {children} kids"), (children, married) if children == 0 && married => ➥println!("Married but no children") _ => println!("Married? {married}. ➥Number of children: {children}."), } }
You can use _
as many times as you want in a match. In this match on colors, we have three to match on, but only check one at a time:
fn match_colors(rgb: (i32, i32, i32)) { match rgb { (r, _, _) if r < 10 => println!("Not much red"), (_, g, _) if g < 10 => println!("Not much green"), (_, _, b) if b < 10 => println!("Not much blue"), _ => println!("Each color has at least 10"), } } fn main() { let first = (200, 0, 0); let second = (50, 50, 50); let third = (200, 50, 0); match_colors(first); match_colors(second); match_colors(third); }
Not much green Each color has at least 10 Not much blue
This example also shows how match
statements work because in the first example, it only prints Not much blue
. But first
also has “not much green.” A match statement always stops when it finds a match and doesn’t check the rest. This is a good example of code that compiles well but is probably not the code you want.
You can make a really big match
statement to fix it, but it is probably better to use a for
loop. We will learn to use for
loops very soon.
Each arm of a match
has to return the same type. So you can’t do this:
fn main() { let my_number = 10; let some_variable = match my_number { 10 => 8, _ => "Not ten", }; }
error[E0308]: `match` arms have incompatible types --> src\main.rs:17:14 | 15 | let some_variable = match my_number { | _________________________- 16 | | 10 => 8, | | - this is found to be of type `{integer}` 17 | | _ => "Not ten", | | ^^^^^^^^^ expected integer, found `&str` 18 | | }; | |_____- `match` arms have incompatible types
The following will also not work for the same reason:
fn main() { let some_variable = if my_number == 10 { 8 } else { "something else "}; let my_number = 10; }
But the following example using if
and else
works because if
and else
are followed by {}, which is a separate scope. The variable some_variable
lives and dies inside a separate scope, and so it has nothing to do with if
and else
:
fn main() { let my_number = 10; if my_number == 10 { let some_variable = 8; } else { let some_variable = "Something else"; } }
You can also use @
to give a name to the value of a match
expression, and then you can use it. In this example, we match an i32
input in a function. If it’s 4 or 13, we want to use that number in a println!
statement. Otherwise, we don’t need to use it:
fn match_number(input: i32) { match input { number @ 4 => println!("{number} is unlucky in China (sounds ➥close to 死)!"), number @ 13 => println!("{number} is lucky in Italy! In ➥bocca al lupo!"), number @ 14..=19 => println!("Some other number that ends ➥with -teen: {number}"), _ => println!("Some other number, I guess"), } } fn main() { match_number(50); match_number(13); match_number(16); match_number(4); }
Some other number, I guess
13 is lucky in Italy! In bocca al lupo!
Some other number that ends with -teen: 16
4 is unlucky in China (sounds close to 死)!
Now let’s move on to the last part of control flow in this chapter: the loop.
With loops, you can tell Rust to repeat something until you tell it to stop. The keyword loop
lets you start a loop that does not stop unless you tell the code when to break
. So this program will never stop:
fn main() { loop {} }
That’s not very helpful, so let’s tell the compiler when it can break the loop (and therefore finish the program):
fn main() { let mut counter = 0; ① loop { counter +=1; ② println!("The counter is now: {counter}"); if counter == 5 { ③ break; } } }
The counter is now: 1 The counter is now: 2 The counter is now: 3 The counter is now: 4 The counter is now: 5
Rust allows you to give a loop a name, which is helpful when you are within a loop that is inside another loop. You can use a '
(called a tick) followed by a colon to give it a name:
fn main() { let mut counter = 0; let mut counter2 = 0; println!("Now entering the first loop."); 'first_loop: loop { ① counter += 1; println!("The counter is now: {}", counter); if counter > 5 { println!("Now entering the second loop."); 'second_loop: loop { ② println!("The second counter is now: {}", counter2); ③ counter2 += 1; if counter2 == 3 { break 'first_loop; ④ } } } } }
② Starts a second loop inside the first loop
③ Now, we are inside 'second_loop.
④ Breaks out of 'first_loop so we can exit the program
If we wrote break;
or break second_loop;
inside this code, the program would never end. The loop would keep on entering 'second_loop
and then would exit but stay inside 'first_loop
, enter 'second_loop
again, and continue forever. Instead, the program completes and prints:
Now entering the first loop. The counter is now: 1 The counter is now: 2 The counter is now: 3 The counter is now: 4 The counter is now: 5 The counter is now: 6 Now entering the second loop. The second counter is now: 0 The second counter is now: 1 The second counter is now: 2
Another kind of loop is called a while
loop. A while
loop is a loop that continues while something is still true
. For each loop, Rust will check whether it is still true
. If it becomes false
, Rust will stop the loop:
fn main() {
let mut counter = 0;
while counter < 5 { ①
counter +=1;
println!("The counter is now: {counter}");
}
}
① Counter < 5` is either true or false.
This prints the same result as the previous code sample that used a counter to keep track of the number of loops, but this time it was much simpler to write:
The counter is now: 1 The counter is now: 2 The counter is now: 3 The counter is now: 4 The counter is now: 5
Another kind of loop is a for
loop. A for
loop lets you tell Rust what to do each time. But in a for
loop, the loop stops after a certain number of times instead of checking to see whether a condition is true
. for
loops use ranges very often. We learned before that
fn main() { for number in 0..3 { println!("The number is: {}", number); } for number in 0..=3 { println!("The next number is: {}", number); } }
The number is: 0 The number is: 1 The number is: 2 The next number is: 0 The next number is: 1 The next number is: 2 The next number is: 3
Also notice that number
becomes the variable name for the numbers from 0..3
. We could have called it n
or ntod_het___hno_f
or anything else. We can then use that name to print the number or do some other operation with it.
If you don’t need a variable name, use _
(an underscore):
fn main() { for _ in 0..3 { println!("Printing the same thing three times"); } }
This prints the same thing three times because there is no number to print each loop anymore:
Printing the same thing three times Printing the same thing three times Printing the same thing three times
If you give a variable name and don’t use it, Rust will tell you:
fn main() { for number in 0..3 { println!("Printing the same thing three times"); } }
This prints the same thing as the previous example. The program compiles fine, but Rust will remind you that you didn’t use number
:
warning: unused variable: `number`
--> src\main.rs:2:9
|
2 | for number in 0..3 {
| ^^^^^^ help: if this is intentional,
➥prefix it with an underscore: `_number`
Rust suggests writing _number
instead of _
. Putting _
in front of a variable name means “Maybe I will use it later.” But using just _
means “I don’t care about this variable at all.” So you can put _
in front of variable names if you will use them later and don’t want the compiler to warn you about them.
You can also use break
to return a value. You write the value right after break
and use a ;
. Here is an example with a loop
and a break
that gives my_number
its value:
fn main() { let mut counter = 5; let my_number = loop { counter +=1; if counter % 53 == 3 { break counter; } }; println!("{my_number}"); }
This code prints 56
. The break counter;
code at the end means “Break and return the value of counter.” And because the whole block starts with let, my_number
gets the value.
Now that we know how to use loops, here is a better solution to our match
problem with colors from before. The new solution is better because we want to compare everything instead of matching and breaking out early when a condition matches. A for
loop is different, as it looks at every item in the way we tell it to do:
fn match_colors(rbg: (i32, i32, i32)) { let (red, blue, green) = (rbg.0, rbg.1, rbg.2); ① println!("Comparing a color with {red} red, {blue} blue, and ➥{green} green:"); let color_vec = vec![(red, "red"), (blue, "blue"), ➥(green, "green")]; ② let mut all_have_at_least_10 = true; ③ for (amount, color) in color_vec { ④ if amount < 10 { all_have_at_least_10 = false; println!("Not much {color}."); } } if all_have_at_least_10 { println!("Each color has at least 10.") } println!(); ⑤ } fn main() { let first = (200, 0, 0); let second = (50, 50, 50); let third = (200, 50, 0); match_colors(first); match_colors(second); match_colors(third); }
① This is a good example of destructuring. We have a tuple called rbg, and instead of using rbg.0, rbg.1, and rbg.2, we can give each item a readable name instead.
② Put the colors in a vec. Inside are tuples with the color names.
③ Use this variable to track if all colors are at least 10. It starts as true and will be set to false if one color is less than 10.
④ Some more destructuring here, letting us give a variable name to the amount and the color name
Comparing a color with 200 red, 0 blue, and 0 green: Not much blue. Not much green. Comparing a color with 50 red, 50 blue, and 50 green: Each color has at least 10. Comparing a color with 200 red, 50 blue, and 0 green: Not much green.
Hopefully, you are starting to feel excited about Rust by now. In the previous chapter, we learned concepts called low level: how computer memory works, ownership of data, and so on. But Rust is also focused on the programmer experience, so the syntax is also very high level in places, as we saw in this chapter. Match statements, ranges, and destructuring are three examples: they are very readable and quick to type, yet no less strict than anything else in Rust.
In the next chapter, we are going to start creating our own types. The tuples that you learned about in this chapter will help you there.
Arrays are extremely fast but have a set size and a single type.
Vectors are sort of like String
s: they are owned types and very flexible.
Tuples hold items that can be accessed with numbers, but they act more like new types of their own rather than indexed collections.
Destructuring is powerful: it lets you pull types apart in almost any way you want.
Ranges are a nice human-readable way to express when something starts and when it ends.
If you have a loop inside a loop, you can name the loops to tell the code which one to break out of.