Variables

In Rust, variables are created with the let statement. By default, this creates an immutable variable. An immutable variable cannot be changed after it has been assigned a value.

fn main() {
    let name = "Rust";
    println!("Hello, {} world!", name);
}

In the above example we use the {} placeholder to insert the value of the name variable into the string. Since Rust 1.58 you can also pass the variable directly in the string format, like this:

fn main() {
    let name = "Rust";
    println!("Hello, {name} world!");
}

It is a matter of taste, but I prefer this method, because it is easier to read and understand. We'll use this method in the rest of the book.

Back to the variables. The let statement creates a variable binding. By default, these bindings are immutable. If we attempt to change this variable, we get a compilation error:

fn main() {
    let name = "Rusty";
    name = "Marcel";
    println!("Welcome, {name} to the Rust world!");
}

Errors and warning returned by the compiler are typically very descriptive and will often provide a hint on how to fix the issue, Do not ignore these comments.

In our case it is suggested to fix it by: make this binding mutable: mut name Let's try that.

fn main() {
    let mut name = "Rusty";
    name = "Marcel";
    println!("Welcome, {name} to the Rust world!");
}

By adding mut to the let statement we have made the variable mutable. This means we can change the value of the variable after it has been assigned.

This is a great moment to introduce you to "Clippy", because our code is suboptimal. If you don't use "on-the-fly" Clippy, you can run it manually double-pressing the "Ctrl" button to open the "Run Anything" window, and type cargo clippy.

Notice the output from Clippy.

warning: value assigned to `name` is never read
 --> src/main.rs:2:13
  |
2 |     let mut name = "Rusty";
  |             ^^^^
  |
  = note: `#[warn(unused_assignments)]` on by default
  = help: maybe it is overwritten before being read?

Clippy is your pedantic Rust friend. Listen to its advice and save yourself a lot of trouble down the road! Run cargo clippy often! It is faster than a regular compilation, and the output is beneficial, especially for rookie Rust developers.

Clippy suggests that we're not reading the name variable, before we're overwriting with a new value. This is an opportunity for optimization, or an indicator that we have a logic error. Let's change the code:

 fn main() {
    let mut name = "Rusty";
    println!("Welcome, {name} to the Rust world!");
    name = "Marcel";
    println!("Welcome, {name} to the Rust world!");
}

Re-run cargo clippy (remember the double-ctrl click) and check the output. Clippy should be happy now.

Type assignment and inference

We can assign a type to a variable with the : operator. For example:

 fn main() {
    let name: &str = "Rusty";
    println!("Welcome, {name} to the Rust world!");
}

Whenever possible, Rust will try to infer the type of the variable. The compiler will look at the value you assign, and it will look ahead in your code to determine how you are using the variable and try to assign a type to it. This is best illustrated with some examples where the type is not immediately obvious.

 fn main() {
    let a = 1;
    let b = 2;
    let c: u32 = a + b;
    println!("c = {c}");
}

If you type the above example live (instead of lazily copying & pasting), you will see that the type of a' and b' changes to u32 the moment you add the line let c: u32 = a + b;. The Rust compiler is smart enough to figure out that without additional type conversions, a and b must be u32 due to the assignment to c later in the code.

A list of built-in data types can be found here.

Exercise

Fix the following example by changing the types of a, b and/or 'c'. Try a few different combinations and see what happens.

fn main() {
    let a: u8 = 128;
    let b: u8 = 128;
    let c: u32 = a + b;
    println!("c = {c}");
}

Shadowing

Rust allows you to shadow a variable. This means you can re-use the same variable name for a new variable. The new variable will shadow the old variable, effectively hiding it from the rest of the code.

fn main() {
    let name = "Rusty";
    println!("Welcome, {name} to the Rust world!");

    let name = "Marcel";
    println!("Welcome, {name} to the Rust world!");
}

You can use this feature to change the type of variable, while keeping the same name. This can be useful when you want to ensure that the old value can no longer accidentally be used.

fn main() {
    let name = "Rusty";
    println!("Welcome, {name} to the Rust world!");

    let name = name.to_uppercase();
    println!("Welcome, !{name}! to the Rust world!");
}

Note that the second name variable has a String type, while the first name variable has a &str type.

Reference material