Variables

Rust by example - Variable bindings Rust by example - constants

Variables are declared with let, following by the variable's name and an (often optional) type annotation. Rust can often infer the type of a variable, but you can provide explicit type annotations.

/// Variables are used to store values.
fn main() {
    // Rust is a statically typed language, meaning that
    // the type of every variable must be known at compile time.
    let a: i32 = -1;

    // However, Rust's compiler is very good at type inference,
    // so you often don't need to explicitly write out the type.
    let b = -2; // Infers an integer (`i32` by default).
    let c = 3.4; // Infers a float (`f64` by default).

    // It is possible to separate the declaration of a variable and its
    // assignment. However, the variable must be assigned before first use.
    let d;
    // println!("d: {d}");
    // ERROR: Used binding `d` is possibly-uninitialized.
    d = "separate assignment";
    println!("d: {d}");

    // Variables have block scope. This means a variable is valid from the point
    // it is declared until the end of the current code block (`{}`) it is
    // within.
    let outer_var = 10;
    println!("Outer var: {outer_var}");
    {
        // Start of an inner scope.
        let inner_var = 20;
        println!("Inner var: {inner_var}");
    } // End of inner scope - `inner_var` goes out of scope.
    // println!("Inner var outside inner scope: {inner_var}"); // ERROR.
    println!("Outer var outside inner scope: {outer_var}");
}

Immutability and Mutability

Variables are immutable by default. Use the mut keyword to make a variable mutable:

fn main() {
    // By default, variables are immutable.
    // Once we give the variable a value, the value won't change.
    // This default immutability helps prevent common programming errors,
    // especially in concurrent programming.
    let i = 5;
    // You cannot change the value of an immutable variable.
    // ERROR: i = 6;
    println!("i: {i}");

    // Rust's immutability is deep, not shallow:
    // it applies to the inner fields of a struct, tuple, array, etc.
    let s = MyStruct { flag: true };
    // s.data = false; // ERROR: cannot assign to `s.data`, as `s` is not
    // declared as mutable.
    println!("s: {s:?}");
    let t = (2, 2);
    // ERROR: t.0 = 1;
    println!("t: {t:?}");
    let arr = [1, 2, 3];
    // ERROR: arr[0] = 2;
    println!("arr: {arr:?}");

    // Declare a mutable variable with `let mut`:
    let mut st = String::new();
    // You can modify or reassign a mutable variable.
    st.push_str("42");
    st = "43".to_string();
    println!("st: {st}");
    // You cannot change its type:
    // ERROR: st = 42;
}

#[derive(Debug)]
struct MyStruct {
    flag: bool,
}

Shadowing

Variables can be redeclared with the same name, "shadowing" the previous variable. The new variable effectively hides the previous one within its scope.

Shadowing is useful when you need to perform a series of transformations on a value, but you no longer need the original value. Instead of creating new variable names for each intermediate step, you can simply reuse the same name.

fn main() {
    // `x` is an immutable variable.
    let x = 5;
    // Its value can't change.
    // ERROR: x = x + 1;

    // But it can be redefined (shadowed).
    // Any subsequent uses of the `x` name within the current scope will refer
    // to the new variable, and the old variable becomes inaccessible by
    // that name. Notice the `let`:
    let x = x + 1;
    // `x` is a _new_ variable that shadows the previous one.
    println!("x: {x}");

    // The type can change by shadowing.
    let x = "example";
    println!("x: {x}");

    // Let's create an inner scope (block) with curly brackets:
    {
        let x = "within scope";
        println!("x: {x}");
        assert_eq!(x, "within scope");
    }
    // When that scope is over, the inner shadowing ends and the previous `x`
    // definition applies:
    println!("x: {x}");
    assert_eq!(x, "example");

    // In contrast, a mutable variable's value can change, but its type can't.
    let mut y = 10;
    y = 20; // Valid: changing the value of `y`
    // y = "hello"; // ERROR: cannot change the type of `y`
    println!("y: {y}");
}

Destructuring

Destructuring allows you to unpack values from compound types like tuples, arrays, structs, or enums into individual variables.

fn main() {
    // Destructuring a tuple.
    // The values in the tuple are assigned to the variables in the same order.
    let (x, y, _) = (1, 2, 3);
    // x is assigned the value 1.
    // y is assigned the value 2.
    // Use `_` to ignore a field you don't care about.

    println!("x: {x}, y: {y}");

    struct Point {
        x: i32,
        y: i32,
    }

    // Create an instance of the `struct` defined above:
    let p = Point { x: 0, y: 7 };

    // Destructuring a struct.
    // The values in the struct are assigned to the variables based on the field
    // names. `a` is assigned the value of `p.x`, which is 0.
    // `b` is assigned the value of `p.y`, which is 7.
    let Point { x: a, y: b } = p;
    println!("a: {a}, b: {b}");

    // Here is a simpler way to destructure a struct:
    let Point { x, y } = p;
    // This is equivalent to `let Point { x: x, y: y } = p;`.
    print!("x and y: {:?}", (x, y));
}

Related Topics

  • Constants and Statics.
  • Match and Patterns.