Rust Fundamentals
Building a Simple Rust Application
Putting It All Together
It's time to apply everything you've learned. We'll build a classic command-line program: a number guessing game. This project will touch on variables, data types, functions, control flow, ownership, and error handling. It's a great way to see how Rust's core concepts work together in a real application.
First, let's create a new project using Cargo. Open your terminal and run this command:
cargo new guessing_game
Cargo generates a new project directory called guessing_game. Inside, you'll find a src folder with a main.rs file and a Cargo.toml file for managing dependencies. Navigate into the new directory:
cd guessing_game
Let's open src/main.rs. We'll start by getting input from the user.
Handling User Input
To read what the user types, we need to bring the input/output functionality from Rust's standard library into scope. We'll also create a mutable string variable to store the user's guess.
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Let's break this down:
use std::io;brings theiomodule into scope.let mut guess = String::new();creates an empty, mutable string. Remember, variables are immutable by default in Rust.io::stdin().read_line(&mut guess)reads the user's input and appends it to ourguessstring. We pass a mutable reference (&mut guess) soread_linecan change the string's content..read_line()returns aResulttype. We use the.expect()method as a simple way to handle theErrvariant, which would crash the program and display our message if something went wrong.
Run the program with cargo run. It will wait for your input, and then print it back to you.
Generating a Secret Number
We need a secret number for the user to guess. For this, we'll use a crate—Rust's term for a package of code. The rand crate provides random number generation. First, we need to add it as a dependency in our Cargo.toml file. Open it and add the following line under [dependencies]:
[dependencies]
rand = "0.8.5"
Now, let's update src/main.rs to generate a random number between 1 and 100.
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Here's what's new:
use rand::Rng;imports theRngtrait, which provides the methods for random number generation.rand::thread_rng()gives us a random number generator that's local to the current thread of execution.gen_range(1..=100)generates a random number within the range of 1 to 100, inclusive.
The first time you run this with cargo run, Cargo will download and compile the rand crate. For now, we're printing the secret number to make testing easier.
Comparing and Looping
Now we need to compare the user's guess with the secret number. The user input, guess, is a String, while secret_number is an integer. We can't compare them directly. We must first convert the string to a number.
let guess: u32 = guess.trim().parse().expect("Please type a number!");
This line shadows the previous guess variable. The .trim() method removes any whitespace, and .parse() converts the string to a number. We specify the type u32 for our new guess variable. Like read_line, parse returns a Result, so we use expect to handle potential errors, like if the user types "cat" instead of a number.
Next, we'll use a match expression to compare the guess to the secret number and a loop to let the user keep guessing until they get it right.
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
The
cmpmethod compares two values and returns a variant of theOrderingenum:Less,Greater, orEqual. Thematchstatement is the perfect way to handle these different outcomes.
When the guess is correct (Ordering::Equal), we print a success message and use break to exit the loop. Once you're confident it's working, you can remove the line that prints the secret_number.
Finally, what if the user enters something that isn't a number? Our program panics. Let's handle that gracefully by ignoring invalid input and letting the user try again.
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
Instead of calling expect, we switch to a match expression on the Result from parse. If it's an Ok variant, we return the number. If it's an Err, we use continue to skip to the next iteration of the loop, ignoring the invalid input.
Here is the complete, final program:
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Congratulations! You've built a complete Rust application from scratch, tying together many of the language's core features in a practical way.
In a Rust project managed by Cargo, where do you declare an external dependency like the rand crate so Cargo will download and compile it?
Consider the following code: let mut guess = String::new(); io::stdin().read_line(&mut guess).expect("Failed to read line");. Why must guess be declared as mutable (mut)?