Back to the basics - conversions
So far we've used the read_string()
function to capture some input from the user. This has been quite useful so far,
but sometimes you need a data type other than text. For example, if you want to work with numbers. In this chapter,
we'll look at how to convert the user's text-based input into a number. We'll also cover the basics of error handling.
We'll start with an earlier example that I've modified slightly:
fn main() { println!("How old are you?"); let age = read_string(); println!("You are {age} years old."); } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); input.trim().to_string() }
Note that I've removed the intermediate
cleaned_input
and now return the result ofinput.trim().to_string()
directly from theread_string()
function. Remember that we can do this by omitting the;
at the end of the last statement in a function.
Capturing age as a number
The goal is to capture the age of the user and convert it into one of the numeric data types: i8
, i16
, i32
, u8
,
u16
or u32
.
Exercise
Consider the range of numbers these data types can hold and choose what would be the best fit.
Now let's modify the above code and add a new read_number
function.
use std::str::FromStr;
fn main() {
println!("How old are you?");
let age = read_number();
println!("You are {age} years old.");
}
fn read_string() -> String {
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("can not read user input");
input.trim().to_string()
}
fn read_number() -> u8 {
let input = read_string();
u8::from_str(&input).expect("input is not a number")
}
As you can see, I've chosen the u8
type. u8
holds numbers from 0 to 255, which should be enough for any living
person at this point. The alternative would have been i8
. i8
goes up to 127. According to wikipedia, the oldest
human was 122, which does not leave us much room. i8
also holds negative numbers, which is not needed in our case.
Since u8
and i8
both cover the same amount of memory, 8 bits or 1 byte, u8
makes the most sense. Anything larger,
like u16
or u32
would be a waste of memory.
There are a few other things that have been added to the code. The RustRover editor highlighted an error when I added
the u8::from_str
related method. Apparently it was missing some code that it needed for this operation and suggested
that I add the line use std::str::FromStr;
. So I did. We'll discuss importing external functions later, for now just
make sure you include the use
statement at the top of your main.rs
file.
The associated u8::from_str
function will try to convert the input
to a u8
number type. This can fail in many
ways: the user could have typed something that isn't a number, or they could have typed a number that isn't in
the u8' range, like
300', or they could have just hit the enter
key and not given us any input.
The expect()
method will deal with these errors. Well... handling the error might be a bit of an exaggeration, it will
make sure that the program does not continue with an invalid return value. In fact, our program will crash with an
output like this:
thread 'main' panicked at 'input is not a number: ParseIntError { kind: Overflow }', src/main.rs:20:26
stack backtrace:
0: rust_begin_unwind
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483
1: core::panicking::panic_fmt
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85
2: core::option::expect_none_failed
at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/option.rs:1234
3: core::result::Result<T,E>::expect
at /Users/mibes/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/result.rs:933
4: hello_world::read_number
at ./src/main.rs:20
5: hello_world::main
at ./src/main.rs:5
6: core::ops::function::FnOnce::call_once
at /Users/mibes/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
The crucial piece of information is: input is not a number: ParseIntError { kind: Overflow }
.
Surely there must be a better way to handle such input errors. Of course there is. Lots of ways, actually. Let's start with the simplest case. We'll provide a default age when someone makes an input error.
use std::str::FromStr; fn main() { println!("How old are you?"); let age = read_number(); println!("You are {age} years old."); } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); input.trim().to_string() } fn read_number() -> u8 { let input = read_string(); u8::from_str(&input).unwrap_or(0) }
As you can see, I replaced the expect()
with unwrap_or(0)
. In Rust, unwrap()
attempts to retrieve a valid value
from the preceding function or method. It is similar to expect()
, but does not provide a meaningful message if it
fails. You should rarely use unwrap
or expect
because it often indicates that you are not handling error conditions
properly.
**However, unwrap
's brother unwrap_or()
can be quite useful. It tries to take a valid value, just like unwrap
,
but if it fails, it returns the default value instead, in our case 0. So unlike unwrap
or expect
, unwrap_or
will not crash your program.
Run the above example and see what happens if you enter an invalid u8
number.
Considering that I have never seen a baby type on a keyboard, returning 0 seems like an acceptable default. We can even check for the 0 in our main function.
use std::str::FromStr; fn main() { println!("How old are you?"); let age = read_number(); if age > 0 { println!("You are {age} years old."); } else { println!("You've entered an invalid age"); } } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); input.trim().to_string() } fn read_number() -> u8 { let input = read_string(); u8::from_str(&input).unwrap_or(0) }
Obviously, returning a default value cannot be used in all cases, and you may even question whether it is legitimate not to accept 0 as a valid input.
In the next chapter, we'll examine the use of optional return values to handle our case in a nicer way.