5.3 Expressions and Statements

Rust makes a clearer distinction between expressions and statements than C/C++.

5.3.1 Expressions

An expression evaluates to a value. Most code constructs in Rust are expressions, including:

  • Literals (5, true, "hello")
  • Arithmetic (x + y)
  • Function calls (calculate(a, b))
  • Comparisons (a > b)
  • Block expressions ({ let temp = x * 2; temp + 1 })
  • Control flow constructs like if, match, and loop (though loop itself often doesn’t evaluate to a useful value unless broken with one).
// These are all expressions:
5
x + 1
is_valid(data)
if condition { value1 } else { value2 }
{ // This whole block is an expression
    let intermediate = compute();
    intermediate * 10 // The block evaluates to this value
}

Critically, an expression by itself is not usually valid Rust code. It needs to be part of a statement (like an assignment or a function call) or used where a value is expected (like the right side of = or a function argument).

5.3.2 Statements

A statement performs an action but does not evaluate to a useful value. Statements end with a semicolon (;). The semicolon effectively discards the value of the preceding expression, making the overall construct evaluate to the unit type ().

Common statement types:

  1. Declaration Statements: Introduce items like variables, functions, structs, etc.
    • let x = 5; (Variable declaration statement)
    • fn my_func() {} (Function definition statement)
    • struct Point { x: i32, y: i32 } (Struct definition statement)
  2. Expression Statements: An expression followed by a semicolon. This is used when you care only about the side effect of the expression (like calling a function that modifies state or performs I/O) and want to discard its return value.
    • do_something(); (Calls do_something, discards its return value)
    • x + 1; (Calculates x + 1, discards the result - usually pointless unless + is overloaded with side effects)

Key Difference from C/C++: Assignment (=) is a statement in Rust, not an expression. It does not evaluate to the assigned value. This prevents code like x = y = 5; (which works in C) and avoids potential bugs related to assignment within conditional expressions (if (x = 0)).

#![allow(unused)]
fn main() {
fn do_something() -> i32 { 0 }
let mut x = 0;
let y = 10; // Declaration statement
x = y + 5;  // Assignment statement (the expression y + 5 is evaluated, then assigned to x)
do_something(); // Expression statement (calls function, discards result)
}

5.3.3 Block Expressions

A code block enclosed in curly braces { ... } is itself an expression. Its value is the value of the last expression within the block.

  • If the last expression lacks a semicolon, the block evaluates to the value of that expression.
  • If the last expression has a semicolon, or if the block is empty, the block evaluates to the unit type ().
fn main() {
    let y = {
        let x = 3;
        x + 1 // No semicolon: the block evaluates to x + 1 (which is 4)
    };
    println!("y = {}", y); // Prints: y = 4

    let z = {
        let x = 3;
        x + 1; // Semicolon: the value is discarded, block evaluates to ()
    };
    println!("z = {:?}", z); // Prints: z = ()

    let w = { }; // Empty block evaluates to ()
    println!("w = {:?}", w); // Prints: w = ()
}

This feature is powerful, allowing if, match, and even simple blocks to be used directly in assignments or function arguments. Be mindful of the final semicolon; omitting or adding it changes the block’s resulting value and type.

5.3.4 Line Structure

Rust is free-form regarding whitespace and line breaks. Statements are terminated by semicolons, not newlines.

#![allow(unused)]
fn main() {
// Valid, spans multiple lines
let sum = 10 + 20 +
          30 + 40;

// Valid, multiple statements on one line (discouraged for readability)
let a = 1; let b = 2; println!("Sum: {}", a + b);
}