This is the second of two chapters touring the standard library, with a lot of new (but not difficult) types that you’ll find useful to know. Near the end, we will look at some macros we haven’t encountered before, which will lead into the next chapter, where you will learn how to write your own!
As the name implies, the std::mem
module has types and functions for dealing with memory. The functions inside this module are particularly interesting (and convenient). We have seen some of them already, such as size_of(), size_of_val()
, and drop()
:
use std::mem;
fn main() {
println!("Size of an i32: {}", mem::size_of::<i32>());
let my_array = [8; 50];
println!("Size of this array: {}", mem::size_of_val(&my_array));
let some_string = String::from("Droppable because it's not Copy");
drop(some_string);
// some_string.clear(); ①
}
① If we uncommented this, it wouldn’t compile.
Size of an i32: 4 Size of this array: 200
Note that in the previous code we didn’t need to write mem::drop()
, just drop()
, because this function is part of the prelude. We will look at the prelude shortly.
Technically, you can call drop()
on a Copy
type, but it will have no effect. As the function documentation states:
This effectively does nothing for types which implement Copy
, e.g., integers. Such values are copied and then moved into the function, so the value persists after this function call. (https://doc.rust-lang.org/std/mem/fn.drop.html)
Let’s look at some other functions in std::mem
.
The swap()
function lets you switch the values between two variables. You use a mutable reference for each to do it. This is particularly helpful when you have two things you want to switch and Rust doesn’t let you because of borrowing rules or because the parameter isn’t an Option
that you could use take()
to replace with None
. (More on the take()
function in a moment.)
Let’s use this function to do some owner swapping of the One Ring from Lord of the Rings:
use std::mem; #[derive(Debug)] struct Ring { owner: String, former_owners: Vec<String>, } impl Ring { ① fn switch_owner_to(&mut self, name: &str) { if let Some(position) = self.former_owners.iter().position(|n| n == ➥name) { mem::swap(&mut self.owner, &mut self.former ➥_owners[position]) ② } else { println!("Nobody named {name} found in former_owners, sorry!"); } } } fn main() { let mut one_ring = Ring { ③ owner: "Frodo".into(), former_owners: vec!["Gollum".into(), "Sauron".into()], }; println!("Original state: {one_ring:?}"); one_ring.switch_owner_to("Gollum"); ④ println!("{one_ring:?}"); one_ring.switch_owner_to("Sauron"); println!("{one_ring:?}"); one_ring.switch_owner_to("Billy"); println!("{one_ring:?}"); }
① This method will try to find a character inside former_owners that matches the search key and, if so, switch owners. We could return a Result or Option here, but to keep it simple, we’ll print an error message if no matching character is found.
② Directly accessing a Vec through an index can be risky, so we’ll use the position method to ensure that we found a String that matches the name we are searching for.
③ For most of the book, the owner of the ring is Frodo, while both Gollum and Sauron are looking for it.
④ Now let’s do some owner switching.
Original state: Ring { owner: "Frodo", former_owners: ["Gollum", "Sauron"] } Ring { owner: "Gollum", former_owners: ["Frodo", "Sauron"] } Ring { owner: "Sauron", former_owners: ["Frodo", "Gollum"] } Nobody named Billy found in former_owners, sorry! Ring { owner: "Sauron", former_owners: ["Frodo", "Gollum"] }
The next function inside std::mem
is called replace()
. It is similar to .swap()
and actually uses swap()
inside. The function is extremely simple:
pub fn replace<T>(dest: &mut T, mut src: T) -> T { swap(dest, &mut src); src }
So, replace()
does a swap and then returns the other item—that’s all there is to it. In other words, .replace()
replaces the value with what you put in and returns the old value, which makes it useful with a let
binding to create a variable. Here’s a quick example:
use std::mem; struct City { name: String, } impl City { fn change_name(&mut self, name: &str) { let former = mem::replace(&mut self.name, name.to_string()); println!("{former} is now called {new}.", new = self.name); } } fn main() { let mut capital_city = City { name: "Constantinople".to_string(), }; capital_city.change_name("Istanbul"); }
This code prints Constantinople
is
now
called
Istanbul
.
Now, let’s get to the function take()
that we previously mentioned. As the name implies, this function outright takes the value from something and returns it. But take()
doesn’t drop the existing variable; instead, it leaves its default value in its place. And that’s why this function requires the type we are take()
-ing from to implement Default
:
pub fn take<T>(dest: &mut T) -> T where T: Default,
So you can do something like this:
use std::mem; fn main() { let mut number_vec = vec![8, 7, 0, 2, 49, 9999]; let mut new_vec = vec![]; number_vec.iter_mut().for_each(|number| { let taker = mem::take(number); new_vec.push(taker); }); println!("{:?}\n{:?}", number_vec, new_vec); }
As you can see, it replaced all the numbers with 0
: no index was deleted:
[0, 0, 0, 0, 0, 0] [8, 7, 0, 2, 49, 9999]
Of course, for your own type you can implement Default
to whatever you want. The following code shows a bank in a country called Klezkavania that gets robbed all the time. Every time it gets robbed, it replaces the money at the front with 50 credits (the default):
use std::mem; #[derive(Debug)] struct Bank { money_inside: u32, money_at_desk: DeskMoney, } #[derive(Debug)] struct DeskMoney(u32); impl Default for DeskMoney { fn default() -> Self { Self(50) ① } } fn main() { let mut bank_of_klezkavania = Bank { ② money_inside: 5000, ② money_at_desk: DeskMoney(500), ② }; let money_stolen = mem::take(&mut bank_of_klezkavania.money_at_desk); println!("Stole {} Klezkavanian credits", money_stolen.0); println!("{bank_of_klezkavania:?}"); }
① default is always 50, not 0.
Stole 500 Klezkavanian credits Bank { money_inside: 5000, money_at_desk: DeskMoney(50) }
You can see that there is always $50 at the desk.
In practice, the take()
function is often used as a convenience method to quickly turn a Some
into a None
without having to do any pattern matching. The following example shows just how short your code can be when using this function:
use std::time::Duration; struct UserState { username: String, connection: Option<Connection>, ① } struct Connection { url: String, timeout: Duration, } impl UserState { fn is_connected(&self) -> bool { self.connection.is_some() } fn connect(&mut self, url: &str) { ② self.connection = Some(Connection { url: url.to_string(), timeout: Duration::from_secs(3600), }); } fn disconnect(&mut self) { self.connection.take(); ③ } } fn main() { let mut user_state = UserState { username: "Mr. User".to_string(), connection: None, }; user_state.connect("someurl.com"); println!("Connected? {}", user_state.is_connected()); user_state.disconnect(); println!("Connected? {}", user_state.is_connected()); }
① We could have the Connection struct hold the state of the connection, but another way to do it is to wrap it in an Option. In this case, Some represents a connected state, and None, a nonconnected state.
② A real connect method would be more complicated than this, but you get the idea.
③ To disconnect, just take() the value and do nothing with it, leaving None in its place.
Connected? true Connected? false
We learned back in chapter 5 that a panic in Rust is actually not very panicky. A panic is simply the program seeing that there is a problem that it can’t deal with, so it gives up. For example, there is nothing a program can do when it sees this code:
println!("{}", vec![1, 2][3]);
The programmer here is telling the program to access the fourth item of a Vec that has two items, and that isn’t allowed, so the only option is to give up. The program then prints a message and unwinds the stack, which cleans up the memory for the thread. And if the thread is the main thread, then the program is over. (Maybe it was named panic because the developer is the one who panics when it happens!)
In any case, because a panic is an orderly process, we can modify its behavior a bit. There is a module in the standard library that is also called std::panic
that lets us modify what happens when a panic takes place.
First, let’s review the output we see whenever a program panics. We’ll start by just using the panic!
macro, which is the easiest way to do it:
fn main() { panic!(); }
thread 'main' panicked at 'explicit panic', src\main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a
➥backtrace
The panic!
macro can take a message, which will change the output somewhat:
fn main() { panic!("Oh man, something went wrong"); }
Now the output contains our message instead of just 'explicit panic'
:
thread 'main' panicked at 'Oh man, something went wrong', src\main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a
➥backtrace
Now let’s take a look at what happens with a panic that happens outside of the panic!
macro. First, we will try to parse a number without unwrapping, which will generate a Result
with the error info. After that, we’ll unwrap and compare the panic info to what we printed out:
fn main() { let try_parse = "my_num".parse::<u32>(); println!("Error output: {try_parse:?}"); let my_num = try_parse.unwrap(); }
We can see that when a panic happens, it first tells us which thread panicked, why it panicked, any error info, the location in the code where the panic happened, and a note about how to display a backtrace:
Error output: Err(ParseIntError { kind: InvalidDigit }) thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ➥ParseIntError { kind: InvalidDigit }', src/main.rs:4:28 note: run with `RUST_BACKTRACE=1` environment variable to display a ➥backtrace
So, that’s the default behavior. But we can change all of this if we want by using a method called set_hook()
. This sets up a global panic hook, which will be called instead of the default panic hook. Inside this method is a closure in which we can do whatever we like when a panic happens. Let’s make a really simple one that prints out a message or two when a panic happens—one in English and another in Korean:
fn main() { std::panic::set_hook(Box::new(|_| { println!("Oops, that didn't work."); println!("앗 뭔가 잘못 됐네요."); ① })); panic!(); }
① Korean for “Oops, something’s gone wrong.”
The panic hook does exactly what we told it to do, and even the location and error information is gone. This is all we see now when we run the program:
Well, that didn't work.
앗 뭔가 잘못 됐네요.
Where does the default information in a panic message come from? It would be nice if we could display that as well.
You might have noticed that inside set_hook()
is a closure with an argument that we ignored by using |_|
. If we give the argument a name, we can see that it is a struct called PanicInfo
, which implements both Debug
and Display
. Let’s print it out:
fn main() { std::panic::set_hook(Box::new(|info| { println!("Well, that didn't work: {info}"); })); panic!(); }
Well, that didn't work: panicked at 'explicit panic', src\main.rs:6:5
The PanicInfo
struct itself is fairly interesting, as it has a parameter called payload
that implements Any
:
pub struct PanicInfo<'a> { payload: &'a (dyn Any + Send), message: Option<&'a fmt::Arguments<'a>>, location: &'a Location<'a>, can_unwind: bool, }
Back in chapter 13 we learned that the Error
trait has methods that let us try to downcast it into a concrete type. The Any
trait is another trait that includes methods for downcasting and is automatically implemented on any type unless it contains a non-'static
reference. In other words, the payload
parameter will hold a trait object behind, which technically could be almost anything.
However, the documentation for the .payload()
method for the PanicInfo
struct tells us which type the payload will usually be:
Returns the payload associated with the panic. This will commonly, but not always, be a &'static str or String.
Let’s give this downcasting a try:
fn main() { std::panic::set_hook(Box::new(|info| { if let Some(payload) = info.payload().downcast_ref::<&str>() { println!("{payload}"); } else { println!("No payload!"); } })); panic!("Oh no"); }
The .downcast_ref::<&str>()
method returns a Some
, so this will simply print Oh no
.
This brings us to an important point: .downcast_ref()
is a method that can fail, so we made sure to use if let
to ensure that a panic did not occur. Avoiding panics is important in any case, but when setting a panic hook, it is especially important because a panic during a panic will lead to an abort, which means it won’t unwind the stack and just hand everything over to the operating system to clean up.
The output for an abort will depend on your operating system, but it can look pretty ugly. On the Playground, a panic inside a panic (in other words, an abort) looks like this:
thread panicked while processing panic. aborting.
timeout: the monitored command dumped core
/playground/tools/entrypoint.sh: line 11: 8 Aborted
➥timeout --signal=KILL ${timeout} "$@"
And on Windows it will look something like this:
thread panicked while processing panic. aborting.
error: process didn't exit successfully: `target\release\rmol.exe` (exit
➥code: 0xc0000409, STATUS_STACK_BUFFER_OVERRUN)
So, be sure to be extra careful when setting a panic hook.
This does bring up an interesting point, though: sometimes people will choose to abort by default instead of panicking because that can reduce the size of the binary by a bit. The smaller size is because there will now be no cleanup code inside the binary. You don’t want to set a panic inside a panic hook to do this, though. Instead, just add this to your Cargo.toml file:
[profile.release] panic = 'abort'
Then run or build the binary in release mode (cargo run –release
or cargo build
–release
), and it will be a little smaller. But the vast majority of the time, you won’t want to abort when a panic happens.
Let’s finish up this section with a somewhat larger (but imaginary) example of where we might want to use a panic hook. Here, we are running some sort of software that accesses a database. We want to make sure that the database gets shut down properly even if there is a panic, before the stack unwinds and the program stops. We’ll also add some pretend types and functions that demonstrate how our system is working and what sometimes goes wrong:
use rand::Rng; struct Database { data: Vec<String>, } fn get_hour() -> u32 { let mut rng = rand::thread_rng(); rng.gen_range(0..=30) ① } fn shut_down_database(hour: u32) -> Result<(), String> { ② match hour { h if (6..18).contains(&h) => { ③ Ok(()) } h if h > 24 => Err(format!("Internal error: hour {h} shouldn't ➥exist")), h => Err(format!("Hour {h} is not working hours, can't shut down")), } } fn main() { std::panic::set_hook(Box::new(|info| { println!("Something went wrong / 문제가 생겼습니다!"); println!("Panic info: {info}"); let hour = get_hour(); match shut_down_database(hour) { Ok(()) => println!("Shutting down database at {hour} ➥o'clock!"), Err(e) => println!("Couldn't shut down database before panic ➥finished: {e}"), } })); let mut db = Database { data: vec![] }; db.data.push("Some data".to_string()); panic!("Database broke"); }
① Uh oh, someone made a mistake, and sometimes the hour of the day is greater than 24. We’ll represent this with a simple function that returns a number up to 30.
② This method will only shut down the database during working hours. Outside of working hours, it will leave it running and log a message; it also checks for incorrect hours of the day.
③ Do some database shutting down stuff.
The output will always include this:
Something went wrong / 문제가 생겼습니다!
Panic info: panicked at 'Database broke', src\main.rs:38:5
The rest of the output will be one of these three lines, depending on the hour of the day returned by get_hour()
:
Couldn't shut down database before panic finished: Internal error: hour 27 ➥shouldn't exist Shutting down database at 17 o'clock! Couldn't shut down database before panic finished: Hour 1 is not working ➥hours, can't shut down
Finally, what if you want to undo the panic hook? That’s easy, just use the take_hook()
method. It looks something like this:
fn main() {
std::panic::set_hook(Box::new(|_| {
println!("Something went wrong / 문제가 생겼습니다!");
}));
let _ = std::panic::take_hook();
panic!();
}
This will just print out the regular thread 'main' panicked at 'explicit panic', src\main.rs:8:5
panic message. We are using let _
because take_hook()
returns the PanicInfo
struct that was set in set_hook()
in the previous example, and we don’t need it here. But if you do need it, you can give it a variable name and use it however you like.
Before we finish this section, let’s take a quick look at the take_hook()
method. Don’t worry about all the details inside, but do you notice a method that we learned in this chapter? There it is—our old friend mem::take()
! You can see that it’s being used to grab the old panic hook, after which it returns it to us:
pub fn take_hook() -> Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send> { if thread::panicking() { panic!("cannot modify the panic hook from a panicking thread"); } let mut hook = HOOK.write().unwrap_or_else(PoisonError::into_inner); let old_hook = mem::take(&mut *hook); drop(hook); old_hook.into_box() }
And since mem::take()
leaves a default value behind, let’s take a look at the Hook
mentioned here to see what it looks like and what its default value is. It’s pretty simple, just an enum that represents either a default panic hook or a custom panic hook.
enum Hook { Default, Custom(Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send>), }
So, at this point in the book, there’s not much code in the standard library that you can’t understand.
The next part of the standard library is very closely related: backtraces.
We learned about backtraces in chapter 14 in the section on testing our code. Viewing backtraces when a panic occurs has been a feature of Rust since the language began. However, being able to view a backtrace at run time is fairly new: it was added with Rust 1.65 in November 2022. Before this, the only way to see backtraces at run time was through a crate called backtrace
.
But now it can be done without any external code, and viewing a backtrace is pretty simple: just use a function called Backtrace::capture()
, which is located in the std::backtrace
module. There is one thing to keep in mind, though. Try running this code on the Playground or your computer and see what happens:
use std::backtrace::Backtrace; fn main() { println!("{}", Backtrace::capture()); }
This only prints out the following:
disabled backtrace
The documentation explains that this method will look for either a RUST_BACKTRACE
or a RUST_LIB_BACKTRACE
environment variable. Interestingly, the source code shows us that it only cares if the environment variables are set to 0
or not:
let enabled = match env::var("RUST_LIB_BACKTRACE") { Ok(s) => s != "0", Err(_) => match env::var("RUST_BACKTRACE") { Ok(s) => s != "0", Err(_) => false, }, };
In other words, this code will still just print disabled backtrace
even though we have a RUST_BACKTRACE
environment variable:
use std::backtrace::Backtrace; fn main() { std::env::set_var("RUST_BACKTRACE", "0"); println!("{:#?}", Backtrace::capture()); }
But anything else will print a backtrace at run time. This will work:
use std::backtrace::Backtrace; fn main() { std::env::set_var("RUST_BACKTRACE", "1"); println!("{}", Backtrace::capture()); }
Setting it to literally anything else will enable capturing a backtrace:
use std::backtrace::Backtrace; fn main() { std::env::set_var("RUST_BACKTRACE", "Hi I'm backtraceㅎㅎㅎ"); println!("{}", Backtrace::capture()); }
Now that it is enabled, the output will look something like what we see in the following example. (It will depend on your operating system, of course.) Here is what the Playground displays:
0: playground::main at ./src/main.rs:5:20 1: core::ops::function::FnOnce::call_once at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library ➥/core/src/ops/function.rs:507:5 2: std::sys_common::backtrace::__rust_begin_short_backtrace at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/ ➥src/sys_common/backtrace.rs:121:18 3: std::rt::lang_start::{{closure}} at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/ ➥src/rt.rs:166:18 4: core::ops::function::impls::<impl core::ops::function::FnOnce<A> for ➥&F>::call_once at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library ➥/core/src/ops/function.rs:606:13 5: std::panicking::try::do_call at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std ➥/src/panicking.rs:483:40 6: std::panicking::try at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std ➥/src/panicking.rs:447:19 7: std::panic::catch_unwind at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std ➥/src/panic.rs:137:14 8: std::rt::lang_start_internal::{{closure}} at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std ➥/src/rt.rs:148:48 9: std::panicking::try::do_call at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std ➥/src/panicking.rs:483:40 10: std::panicking::try at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std ➥/src/panicking.rs:447:19 11: std::panic::catch_unwind at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std ➥/src/panic.rs:137:14 12: std::rt::lang_start_internal At /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std ➥/src/rt.rs:148:20 13: std::rt::lang_start at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std ➥/src/rt.rs:165:17 14: main 15: __libc_start_main 16: _start
That was pretty easy. Now let’s finish up with an example that combines both a panic hook and a backtrace.
The Backtrace
struct also has a method called status()
that returns an enum called a BacktraceStatus
. Instead of just printing out the Backtrace
struct, we can also match on the BacktraceStatus
enum. The enum is quite simple but is interesting for two reasons:
It has the #[non_exhaustive]
attribute, meaning that it may be added to later. That means that you have to match on any extra possible variants after the three listed in the enum, just in case new variants get added later on.
One of the variants is Unsupported
, since some architectures don’t support backtraces:
#[non_exhaustive] pub enum BacktraceStatus { Unsupported, Disabled, Captured, }
use std::{ backtrace::{Backtrace, BacktraceStatus::*}, panic, }; fn main() { panic::set_hook(Box::new(|_| { println!("Panicked! Trying to get a backtrace..."); let backtrace = Backtrace::capture(); ① match backtrace.status() { ② Disabled => println!("Backtrace isn't enabled, sorry"), Captured => println!("Here's the backtrace!!\n{backtrace}"), Unsupported => println!("No backtrace possible, sorry"), ③ // Do some database shutting down stuff } })); std::env::set_var("RUST_BACKTRACE", "0"); ④ panic!(); }
① When a panic happens, we’ll try to capture a backtrace.
② The code matches on the BacktraceStatus enum to see whether a backtrace has been enabled.
③ It’s pretty rare to find an architecture that wouldn’t support a backtrace, but if so, we would see this message.
④ Finally, we enable or disable the backtrace.
Panicked! Trying to get a backtrace... Backtrace isn't enabled, sorry
Rust’s precise error handling means that backtraces aren’t used as much as backtraces (also known as stack traces) in other languages because you don’t usually have to sift through a backtrace to find out what has gone wrong in your code. But the option is always there if you need the extra insight.
The prelude in the standard library is the reason why you don’t have to write things like use std::vec::Vec
to use a Vec
or std::result::Result::Ok()
instead of Ok()
. You can see all the items in the documentation (http://mng.bz/2768) and will already know almost all of them.
There is an attribute called #![no_implicit_prelude]
that disables the prelude. Let’s give it a try and watch just how hard it becomes to write even the simplest of code:
#![no_implicit_prelude] fn main() { let my_vec = vec![8, 9, 10]; let my_string = String::from("This won't work"); println!("{my_vec:?}, {my_string}"); }
Now Rust has no idea what you are trying to do:
error: cannot find macro `println` in this scope --> src/main.rs:5:5 | 5 | println!("{:?}, {}", my_vec, my_string); | ^^^^^^^ error: cannot find macro `vec` in this scope --> src/main.rs:3:18 | 3 | let my_vec = vec![8, 9, 10]; | ^^^ error[E0433]: failed to resolve: use of undeclared type or module `String` --> src/main.rs:4:21 | 4 | let my_string = String::from("This won't work"); | ^^^^^^ use of undeclared type or module `String` error: aborting due to 3 previous errors
For this simple code, you need to tell Rust to use the extern
(external) crate called std
and then the items you want. Here is everything we have to do just to create a Vec
and a String
and print it:
#![no_implicit_prelude] extern crate std; ① use std::convert::From; fn main() { ② let my_vec = std::vec![8, 9, 10]; let my_string = std::string::String::from("This won't work"); std::println!("{my_vec:?}, {my_string}"); }
① We told Rust with #![no_implicit_prelude] that we won’t be bringing in anything from std, so we have to let the compiler know again that we will use it.
② To write even this simple code, we need the vec! macro, String, From (to convert from a &str to a String), and println! to print.
Now it finally works, printing [8, 9, 10], This won't work
. So you can see why Rust has a prelude—it would be a horrible experience without it.
You might be wondering why we haven’t see the extern
keyword before. It’s because you don’t need it that much anymore. Up until 2018, you had to use this keyword when bringing in an external crate. So, to use rand
in the past, you had to write extern crate rand;
, followed by use
statements for whatever else you wanted to bring into scope. But the Rust compiler doesn’t need this help anymore; you can just use use
, and it knows where to find it. So you almost never need extern crate
anymore. But in other people’s Rust code, you might still see it from time to time.
We are getting close to the next chapter, where we will learn to write our own macros. But there are still quite a few macros inside the standard library that we haven’t taken a look at yet, so let’s learn them first. As is the case with the other macros we have used, they are all extremely easy to use and have a bit of a magical feel to them (until we learn in the next chapter how they work internally, that is).
The unreachable!
macro is kind of like todo!
except it’s for code that will never be executed. Maybe you have a match
in an enum that you know will never choose one of the arms, so the code can never be reached. If that’s so, you can write unreachable!
so the compiler knows that it can ignore that part.
For example, let’s say you are using an external crate for a financial tool, and it includes a big enum with all the major banks. We’re going to do a match on it, but as we look at the list we notice something:
enum Bank { BankOfAmerica, Hsbc, Citigroup, DeutscheBank, TorontoDominionBank, SiliconValleyBank // And so on... }
Silicon Valley Bank is no more! We are 100% sure that customers will never choose it, so we don’t want to mark this variant as todo!
or unimplemented!
. We’re never going to implement it. This is definitely a case for the unreachable!
macro:
enum Bank { BankOfAmerica, Hsbc, Citigroup, DeutscheBank, TorontoDominionBank, SiliconValleyBank // And so on... } fn get_swift_code(bank: &Bank) -> &'static str { use Banks::*; match bank { BankOfAmerica => "BOFAUS3N", Hsbc => "HSBCHKHHXXX", Citigroup => "CITIUS33XXX", DeutscheBank => "DEUTINBBPBC", TorontoDominionBank => "TDOMCATTTOR", SiliconValleyBank => unreachable!() } }
Another case for unreachable!
is when the compiler can’t see something that we can. The following example shows a function that gives a random number from 0 to 3 as a usize
, followed by another one called human_readable_rand_num()
that gives a human-readable version of the output: zero
instead of 0, one
instead of 1
, and so on. We are 100% certain that the function will never see any number that isn’t in the range of 0..=3
, but the compiler doesn’t know this. The unreachable!
macro is perfect in this situation:
use rand::{thread_rng, Rng}; fn zero_to_three() -> usize { let mut rng = thread_rng(); rng.gen_range(0..=3) } fn human_readable_rand_num() -> &'static str { match zero_to_three() { 0 => "zero", 1 => "one", 2 => "two", 3 => "three", _ => unreachable!(), } }
unreachable!
is nice for others reading your code as a reminder of how code works: it’s an assertion that something will never happen. You have to be sure that the code is actually unreachable, though. If the compiler ever calls unreachable!
, the program will panic. Just like todo!
, the responsibility is on us to make sure that the macro is never called.
On a related note, you’ll see the word unreachable (not the macro unreachable!
) when the compiler can determine that some code will never be run. Here is a quick example:
fn main() { let true_or_false = true; match true_or_false { true => println!("It's true"), false => println!("It's false"), true => println!("It's true"), } }
Here, the compiler knows that the match will never reach the third line because it has already checked for both of the possible patterns: true
and false
.
warning: unreachable pattern --> src/main.rs:7:9 | 7 | true => println!("It's true"), | ^^^^ |
You’ll see this “unreachable pattern” warning unexpectedly sometimes. The following code creates an enum representing the four seasons and a function that matches on each season. Take a close look at the code and see whether you can tell why the compiler is going to warn us that there are unreachable parts of the code:
pub enum Season { Spring, Summer, Autumn, Winter } pub fn handle_season(season: Season) { use Season::*; match season { Spring => println!("Spring"), summer => println!("Summer"), Autumn => println!("Autumn"), Winter => println!("Winter") } }
Now let’s take a close look at the output:
warning: unreachable pattern --> src/lib.rs:13:9 | 12 | summer => println!("Summer"), | ------ matches any value 13 | Autumn => println!("Autumn"), | ^^^^^^ unreachable pattern | = note: `#[warn(unreachable_patterns)]` on by default warning: unreachable pattern --> src/lib.rs:14:9 | 12 | summer => println!("Summer"), | ------ matches any value 13 | Autumn => println!("Autumn"), 14 | Winter => println!("Winter") | ^^^^^^ unreachable pattern
Can you see it? We made a typo when we tried to match on Summer
, writing summer
instead. Instead of matching on an enum variant, we created a wildcard variable called summer
that will match on anything. And since it matches on anything, the code will never reach the Summer
and Winter
parts of the match statement.
These four macros are incredibly easy and are just used to display the current location in the code. Here they are together:
These can be useful when generating error input or even just to print out hints for yourself that there are oddities to check out in the code. We’ll use the previous Bank
enum example again to illustrate this. In the following code, we are starting to put some modules together to handle bank customers, and this time, we think there might be some Silicon Valley Bank customers still somewhere in the system. Instead of panicking, though, we’ll print out a warning and give the location in the code to make it easy to find and devise a fix:
pub mod input_handling { pub struct User { pub name: String, pub bank: Bank, } #[derive(Debug, Clone, Copy)] pub enum Bank { BankOfAmerica, Hsbc, Citigroup, DeutscheBank, TorontoDominionBank, SiliconValleyBank, } pub mod user_input { use crate::input_handling::{Bank, User}; pub fn handle_user_input(user: &User) -> Result<(), ()> { match user.bank { Bank::SiliconValleyBank => { println!( "Darn it, looks like we have to handle this variant ➥even though Silicon Valley Bank doesn't exist ➥anymore: {}:{}:{}:{}", module_path!(), file!(), column!(), line!() ); Ok(()) } other_bank => { println!("{other_bank:?}, no problem"); Ok(()) } } } } } use crate::input_handling::{user_input::handle_user_input, Bank, User}; fn main() { let user = User { name: "SomeUser".to_string(), bank: Bank::SiliconValleyBank, }; handle_user_input(&user).unwrap(); let user2 = User { name: "SomeUser2".to_string(), bank: Bank::TorontoDominionBank, }; handle_user_input(&user2).unwrap(); }
Darn it, looks like we have to handle this variant even though Silicon ➥Valley Bank doesn't exist anymore: playground::input_handling::user ➥_input:src/main.rs:25:28 TorontoDominionBank, no problem
This macro is similar to the lazy_static!
macro that we saw in the lazy_static
crate, except that the global content is local to the thread in which it is contained. Or rather, it might be more accurate to say that lazy_static!
is similar to thread_ local!
because thread_local!
is much, much older—it was released along with Rust version 1.0.0!
In any case, when this macro is used, you can create a static
that will have the same initial value in every thread in which it is used. The value can then be accessed with a method called .with()
that gives access to the value inside within a closure.
The easiest way to see how this works is with a simple example that compares it with the lazy_static
behavior that we already know. The following code contains some test functions and should be run with cargo test -- --nocapture
so that you can see the output. Remember: each test runs on its own thread!
use std::cell::RefCell; use std::sync::Mutex; lazy_static::lazy_static! { static ref INITIAL_VALUE: Mutex<i32> = Mutex::new(10); ① } thread_local! { static LOCAL_INITIAL_VALUE: RefCell<i32> = RefCell::new(10); ② } #[test] ③ fn one() { let mut lock = INITIAL_VALUE.lock().unwrap(); println!("Test 1. Global value is {lock}"); *lock += 1; println!("Test 1. Global value is now {lock}"); LOCAL_INITIAL_VALUE.with(|cell| { let mut lock = cell.borrow_mut(); println!("Test 1. Local value is {lock:?}"); *lock += 1; println!("Test 1. Local value is now {lock:?}\n"); }); } #[test] fn two() { let mut lock = INITIAL_VALUE.lock().unwrap(); println!("Test 2. Global value is {lock}"); *lock += 1; println!("Test 2. Global value is now {lock}"); LOCAL_INITIAL_VALUE.with(|cell| { let mut lock = cell.borrow_mut(); println!("Test 2. Local value is {lock:?}"); *lock += 1; println!("Test 2. Local value is now {lock:?}\n"); }); } #[test] fn three() { let mut lock = INITIAL_VALUE.lock().unwrap(); println!("Test 3. Global value is {lock}"); *lock += 1; println!("Test 3. Global value is now {lock}"); LOCAL_INITIAL_VALUE.with(|cell| { let mut lock = cell.borrow_mut(); println!("Test 3. Local value is {lock:?}"); *lock += 1; println!("Test 3. Local value is now {lock:?}\n"); }); }
① This INITIAL_VALUE is accessible to all threads. We have to wrap it in a thread-safe Mutex or RwLock.
② However, LOCAL_INITIAL_VALUE is a static that is local to each thread—no need for a Mutex! A regular RefCell or Cell works just fine.
③ Now, we have three tests, each of which does the exact same thing. Each test increments INITIAL_VALUE by 1 and prints it and then increments LOCAL_INITIAL_VALUE by 1 and prints it.
Each test is running on its own thread, so the order will always be different, but the output will look something like this:
running 3 tests Test 3. Global value is: 10 Test 3. Global value is now: 11 Test 3. Local value is 10 Test 3. Local value is now 11 Test 1. Global value is: 11 Test 1. Global value is now: 12 Test 1. Local value is 10 Test 1. Local value is now 11 Test 2. Global value is: 12 Test 2. Global value is now: 13 Test 2. Local value is 10 Test 2. Local value is now 11
As you can see, by the end of all three tests, the INITIAL_VALUE
(the global value) is now 13. But the LOCAL_INITIAL_VALUE
(the thread-local value) starts at 10 inside each thread, and the other tests don’t affect it.
If you check the documentation for LocalKey
(the type created by the macro; https://doc.rust-lang.org/std/thread/struct.LocalKey.html), you’ll see a lot of methods that look like the methods for Cell
and RefCell
. These methods were experimental for quite some time, but were stabilized shortly before the publication of this book in Rust 1.73!
We know that you can use attributes like #[cfg(test)]
and #[cfg(windows)]
to tell the compiler what to do in certain cases. When you have the #[test]
attribute, Rust will run the code when running a test. And when you use windows
, it will run the code if the user is using Windows. But maybe you just want to change one tiny bit of code depending on the configuration. That’s when this macro is useful. It returns a bool
:
fn main() {
let helpful_message = if cfg!(target_os = "windows") {
"backslash"
} else {
"slash"
};
println!("...then type the directory name followed by a
➥{helpful_message}. Then you...");
}
This will print differently, depending on your system. The Rust Playground runs on Linux, so it will print
...then in your hard drive, type the directory name followed by a slash.
➥Then you...
The cfg!
macro works for any kind of configuration. Here is an example of a function that runs differently when you use it inside a test. We have a UserFile
enum that can hold either real data (a File
) or test data (a String
). If this code is run inside main()
, the open_file()
function will open up the main.rs
file and pass it on. If run inside a test, though, it will simply create a dummy String
and pass that on instead. Try running this code with Run
and with Test
in the Playground (or cargo run
and cargo test
on your computer) to see the difference in behavior:
use std::fs::File; use std::io::Read; #[derive(Debug)] enum UserFile { Real(File), Test(String), } fn open_file() -> UserFile { if cfg!(test) { UserFile::Test(String::from("Just a test file")) } else { UserFile::Real(File::open("src/main.rs").unwrap()) } } fn get_file_content() -> String { let mut content = String::new(); let file = open_file(); match file { UserFile::Real(mut f) => { f.read_to_string(&mut content).unwrap(); content } UserFile::Test(s) => s, } } #[test] fn test_file() { let content = get_file_content(); println!("Content is: {content}"); assert_eq!(content, "Just a test file"); } fn main() { let content = get_file_content(); println!("{content}"); }
When this code is run with cargo run
, it will print the entire content of the main.rs
file, but during a test with cargo test -- --nocapture
, it will simply print the following:
running 1 test Content: Just a test file test test_file ... ok
Hopefully, you found this tour of the standard library relaxing and fruitful. There are a lot of hidden gems in the standard library that even experienced Rust users haven’t used before, so feel free to do your own tour and see what you can find. There are also a lot of experimental methods that might be stabilized one day, so check the tracking problems to see what work is being done if you find a method that you like in particular. And remember, experimental doesn’t mean unsafe! An experimental method is just a method that might be stabilized one day or might be thrown out if it doesn’t make sense to stabilize it.
After these two relaxing chapters, there is only one difficult chapter left to go, and it’s the next one: making your own macros!
The functions in the std::mem
module are really convenient for writing shorter code and getting around lifetime problems.
With a panic hook, you can create your own behavior when a panic happens.
You can shrink your binary size a bit by setting the panic behavior to abort instead of unwinding the stack.
A backtrace is now easy to capture at run time without needing an external crate to do it. You will probably see the backtrace
crate in a lot of external code, though, since the backtrace module is a recent addition to the standard library.
The cfg!
macro is a quick way to write code that reacts differently depending on the operating system or any other configuration.
The thread_local!
macro lets you create static values that don’t get shared between threads.