Rust track

Lesson 02 · Part 1: Basics

Variables & Types

Variable bindings, mutability, primitive types and type inference

Part 1: Basics5 min readFree lesson

Variables in Rust are basically immutable. It is a much more conservative starting point than "variables" in other languages, but thanks to it, concurrency and memory safety can be statically guaranteed. In this tutorial, you will learn the basic operation of let, the difference between mut and shadowing, constants (const/static), and type inference and explicit grammar.

Learning Objectives

  • Understand that the value grouped with let is a basic immutable and distinguish it from let mut.
  • Learn how to overwrite a new variable with the same name through shadowing.
  • Explains the difference between const and static and the need for an explicit type.
  • Know how Rust’s type inference works, and specify types where necessary.

Key concepts

let x = 10;
x = 20; // ❌ cannot assign twice to immutable variable `x`

The reason immutability is fundamental is simple. If you start with read-only, later when you read the code you'll wonder "Where did this value change?" No need to keep track. Data races are also reduced.

If the value really needs to change, add mut. However, it is still not possible to assign different types to the same variable.

let mut count = 0;
count += 1;       // OK
//count = "0";   // ❌ Type cannot be changed

If you use let name = ...; one more time, a new variable is created with the same name. There are two key differences from mut.

  • The type can be changed (e.g. &str → usize).
  • Leave the existing variable as is and “overlay” the new variable on top of it — hence shadowing.

4) const and static

Keyword Evaluation Point Specify type memory change
let runtime Inference/Specification stack/heap mut Siman
const compile time Required Inlining Not possible
static Start Runtime Required fixed address unsafe required

By convention, the constant name is SCREAMING_SNAKE_CASE.

For most variables, the type is inferred from the value on the right-hand side. Where the inference is ambiguous, clarification is required.

let parsed: i32 = "42".parse().expect("is not a number."); //The return type of parse is ambiguous.
let pi = 3.14f32;                                   //Can also be specified with the suffix.

See as example

//Part 2 Example 1: let immutable variable and let mut mutable variable
// `cargo run --bin ex01_basics`

fn main() {
    //Basic variables are “immutable”. Once a value is bound, it cannot be changed again.
    let x = 10;
    println!("x = {x}");

    //The `mut` keyword is required to reassign to the same variable.
    let mut y = 5;
    println!("First y = {y}");
    y = y + 1;
    println!("After increase y = {y}");

    //The type can also be specified. (If omitted, Rust infers)
    let pi: f64 = 3.14;
    let flag: bool = true;
    println!("pi = {pi}, flag = {flag}");
}
//Part 2 Example 2: Shadowing — Overwriting a new variable with the same name
// `cargo run --bin ex02_shadowing`

fn main() {
    //Shadowing is regrouping the same name with `let`.
    //Unlike `mut`, the type can also be changed, resulting in a "new variable".
    let value = 5;
    let value = value + 1;       // 6
    let value = value * 2;       // 12
    println!("value = {value}");

    //Shadowing that changes the type
    let spaces = "   ";          // &str
    let spaces = spaces.len();   // usize
    println!("Space length = {spaces}");

    //Not possible with mut: Other Compilation error occurs when assigning type.
    // let mut s = "   "; s = s.len(); // ❌
}
//Part 2 Example 3: const and static
// `cargo run --bin ex03_consts`

//const is determined at compile time and must specify the type.
const MAX_USERS: u32 = 100_000;

//static exists in the same memory throughout the life of the program.
static APP_NAME: &str = "Lecture_Rust";

fn main() {
    println!("APP_NAME  = {APP_NAME}");
    println!("MAX_USERS = {MAX_USERS}");

    //`_` in a numeric literal is a readability separator (has no effect on the value)
    let big = 1_000_000;
    println!("big = {big}");

    //Type suffix — 23i64, Can be specified as 1.5f32
    let count = 23i64;
    let ratio = 1.5f32;
    println!("count = {count}, ratio = {ratio}");
}

Common Mistakes

A. Shadowing is useful for naturally expressing type-changing conversion. For example, when converting an input string to a number, you can only change the form with the same name, such as let input: String = ...; → let input: i32 = input.parse()...;. mut is used when updating only the value of the same variable.

A. The value of const must be confirmed at compile time and the type must be specified (e.g. const PI: f64 = 3.14;). let can also receive runtime values, and the type is usually inferred. If possible, leave what appears to be a function internal constant as const to make the intention clear.

A. This is a separator for readability. It is exactly the same value as 1000000, and the position is free (same as 1_00_00_00). It is often used for large numbers or hexadecimal numbers (0xFF_FF).

A. Usually, the compiler infers it even if you don't specify it. However, if the return type is general like .parse(), you must write the type in the variable or use TurboFish syntax like parse::<i32>().

Cleanup

  • The basic variable is specified as immutable, and variable is specified as mut.
  • Shadowing allows you to create a new variable with the same name and change its type.
  • const is a compile-time constant, and static is a static variable that lives throughout the life of the process.
  • Type inference is strong, but explicit when ambiguous (let x: i32 = ...; or Turbofish).

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/variables/Cargo.tomlTOML
[package]
name = "variables"
version = "0.1.0"
edition = "2021"
default-run = "variables"

[dependencies]
src/variables/src/main.rsRust
//Just run `cargo run` and this guidance message will be displayed.
//Run each example by specifying the bin name as shown below.

fn main() {
    println!("This crate has three running examples.");
    println!();
    println!("  cargo run --bin ex01_basics");
    println!("  cargo run --bin ex02_shadowing");
    println!("  cargo run --bin ex03_consts");
}

Assignment

  • File name: homework_01/src/main.rs
  • Key concepts: let mut, arithmetic substitution

Requirements

  • Starts with let mut counter = 0;.
  • Counters are sequentially +5, *3, -2 and then output the value at each step.
  • Prints the final value on the last line.

Expected output

Start: 0
5 After: 5
*3 After: 15
-2 After: 13
Final: 13

Hint

  • Use counter += 5;, counter *= 3;, and counter -= 2;.

  • Check that compilation is blocked if you try without mut.

  • File name: homework_02/src/main.rs

  • Key concepts: shadowing, changing type

Requirements

  • Define the string as let input = "12345";.
  • Shadow with the same name input and convert it to i32.
  • Outputs the converted value plus 100.

Expected output

Original string: 12345
Integer conversion: 12345
+100: 12445

Hint

  • let input: i32 = input.parse().expect("Not a number");
  • Shadowing is grouping the same name again as let without mut.

Check the correct answer

After solving it yourself, compare it with the reference solution below. The reference solution is annotated with key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/Cargo.tomlTOML
[package]
name = "homework_01"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_01/src/main.rsRust
//Key points:
//- The same variable needs to be updated, so `mut` is needed.
//- Compound assignment operators such as ` =`, `*=`, and `-=` improve readability.

fn main() {
    let mut counter = 0;
    println!("Start: {counter}");

    counter += 5;
    println!("After 5: {counter}");

    counter *= 3;
    println!("*3 After: {counter}");

    counter -= 2;
    println!("-2 After: {counter}");

    println!("Final: {counter}");
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key points:
//- Shadowing creates a new variable by regrouping the same name with `let`. You can change the type.
//- Since the results of parse are generalized, the type must be indicated by specifying `: i32` on the left side.

fn main() {
    let input = "12345";
    println!("Original string: {input}");

    //Shadowing:
    let input: i32 = input.parse().expect("is not a number.");
    println!("Integer conversion: {input}");

    let plus = input + 100;
    println!("+100: {plus}");
}

Learn this lesson with a mentor

Work through the Rust track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now