4.7Introduction to if expressions
Until now, your programs have run every statement, top to bottom, every time. Useful programs are choosier. The chooser is if, it works the way you'd guess for about three paragraphs, and then Rust plays the card it's been palming since lesson 1.11.
The basic form
fn main() {
let temperature = 38;
if temperature > 30 {
println!("It's hot out.");
}
println!("Weather report complete.");
}It's hot out.
Weather report complete.
if, then a condition, then a brace-wrapped block that runs only when the condition is true. The condition must be an actual bool (lesson 4.6 showed the compiler enforcing that), and the comparison operators are how conditions usually get made.
Two syntax notes for readers arriving from other languages, both enforced rather than suggested. There are no parentheses around the condition; write them anyway and the compiler tidily objects:
warning: unnecessary parentheses around `if` condition
--> src/main.rs:3:8
|
3 | if (temperature > 30) {
| ^ ^
|
= note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
help: remove these parentheses
|
3 - if (temperature > 30) {
3 + if temperature > 30 {
|
And the braces are mandatory, even for a single statement. C-family languages make braces optional there, which spawned a famous bug genus: add a second statement under a brace-less if, and it silently runs unconditionally, indentation notwithstanding (a vulnerability in Apple's TLS code, the celebrated "goto fail" bug, was approximately this). Rust deletes the genus: no braces, no compile.
else and else if
else supplies the other path, and else if chains more conditions, checked top to bottom, first match wins:
fn main() {
let n = -4;
if n > 0 {
println!("{n} is positive");
} else if n < 0 {
println!("{n} is negative");
} else {
println!("{n} is zero");
}
}-4 is negative
So far, so familiar. Now the twist.
if is an expression
In Rust, if doesn't just direct traffic; the whole if/else evaluates to a value, which means it can sit anywhere a value can:
fn main() {
let score = 87;
let grade = if score >= 60 { "pass" } else { "fail" };
println!("{grade}");
}pass
You knew this was coming. Blocks are expressions whose value is their tail expression (lesson 1.11); an if/else picks which block runs, so its value is the chosen block's value. { "pass" } and { "fail" } are blocks with tail expressions; the if selects one; grade receives it. The machinery is old, only the application is new.
Two rules keep it sound, and both are enforced with errors you can now read fluently. First, the arms must agree on type, because grade has to be some one type at compile time:
let grade = if score >= 60 { "pass" } else { 0 };error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:3:50
|
3 | let grade = if score >= 60 { "pass" } else { 0 };
| ------ ^ expected `&str`, found integer
| |
| expected because of this
Second, value-position if requires an else. Without one, what would grade be when the condition is false? The compiler declines to invent an answer:
let grade = if score >= 60 { "pass" };error[E0317]: `if` may be missing an `else` clause
--> src/main.rs:3:17
|
3 | let grade = if score >= 60 { "pass" };
| ^^^^^^^^^^^^^^^^^------^^
| | |
| | found here
| expected `&str`, found `()`
|
= note: `if` expressions without `else` evaluate to `()`
= help: consider adding an `else` block that evaluates to the expected type
(Read that note line with a 1.11 eye: a lone if is fine as a statement, where its () bothers no one, exactly like your first example in this lesson.)
If you've used C-family languages, you may recognize what this replaces: the ternary operator cond ? a : b, a separate compressed syntax for "choose a value." Lesson 1.10 promised its obituary, and here it is: Rust's if already is an expression, so no second syntax is needed. One construct, statement or value, your choice.
Best practice
Use the expression form when both arms exist to produce a value (let label = if ... { a } else { b }; beats declaring a mut and assigning in branches, on the same reasoning as lesson 1.12's preferred solution). Use the statement form when the branches exist to do things. When an expression-form if grows past a line or two per arm, let it be a statement again; readability outranks cleverness.
An old promise, kept
Lesson 1.4's warning box told you =/== confusion is a classic bug in other languages and promised this lesson would show you Rust's defense. In C++, if (x = 5) compiles: it assigns 5, the assignment produces 5, 5 is truthy, the branch always runs, and the bug ships. Watch the same typo here:
fn main() {
let mut x = 3;
if x = 5 {
println!("five");
}
}error[E0308]: mismatched types
--> src/main.rs:3:8
|
3 | if x = 5 {
| ^^^^^ expected `bool`, found `()`
|
help: you might have meant to compare for equality
|
3 | if x == 5 {
| +
Two lessons conspired: assignment is an expression producing () (lesson 1.11), and conditions must be bool (lesson 4.6), so the typo has nowhere to hide, and the help line even diagnoses your intent. This is the course's thesis in one error message: same human mistake, but here it's a labeled compile error instead of a shipped bug.
Quiz time
Question #1
What does this print?
fn main() {
let age = 70;
if age < 13 {
println!("child ticket");
} else if age < 65 {
println!("adult ticket");
} else {
println!("senior ticket");
}
}Show solution
senior ticket
70 fails < 13, fails < 65, and lands in the else. Note the chain's elegance: each else if only needs to handle what earlier conditions didn't catch, so "adult" is just < 65, not "≥ 13 and < 65."
Question #2
Write a function describe(n: i32) -> &'static str that returns "positive", "negative", or "zero", using if as an expression (one let-free function body). Don't worry about the unfamiliar return type spelling; it's "a string like a literal," properly explained in chapter 9, and the compiler will accept exactly this spelling.
Show solution
fn describe(n: i32) -> &'static str {
if n > 0 {
"positive"
} else if n < 0 {
"negative"
} else {
"zero"
}
}
fn main() {
println!("{}", describe(12));
println!("{}", describe(-3));
println!("{}", describe(0));
}
The whole if/else if/else is the function's tail expression; each arm's tail is a string. No return, no temporary variable, every path produces a value.
Question #3
Predict the compiler's reaction, precisely:
fn main() {
let logged_in = false;
let message = if logged_in { "welcome" };
println!("{message}");
}Show solution
E0317: if may be missing an else clause. In value position, the false case must produce something, and () doesn't match "welcome"'s type. Fix: add else { "please log in" } (or any &str).
Decisions unlocked. Next, the type that's been hiding inside every string you've printed: char, where Rust's Unicode story begins.