$elementary.dev
[Courses][About]|
$elementary.dev

Text-first, project-driven courses for developers who want to level up by building real things.

CoursesAbout
Stay Updated

Get notified about new courses and articles.

// No spam, unsubscribe anytime

© 2026 elementary.dev
PrivacyTerms
// Course

Elementary Rust

2/11
  • 01Hello, Rust
  • 02Variables and Types
  • 03Functions and Control Flow
  • 04Why Memory Management Matters
  • 05Ownership: Rust's Solution
  • 06Borrowing and References
  • 07Slices
  • 08Structs
  • 09Enums and Pattern Matching
  • 10Organizing Rust Projects
  • 11Collections
courses/elementary-rust/02-variables-types

Variables and Types

Master Rust's approach to variables, mutability, and its rich type system.

~20 min|#Lesson 2/11

Variables and Types

In most languages, variables are mutable by default. Rust flips this. Every variable is immutable unless you explicitly say otherwise.

let x = 5;
x = 6; // Error: cannot assign twice to immutable variable

This fails to compile. The compiler protects you from accidentally mutating data that shouldn't change.

Why Immutability by Default?

Think about the bugs you've chased down. How many were caused by some variable changing when you didn't expect it? Rust eliminates an entire category of bugs by making you think about mutation upfront.

Immutable variables make code easier to reason about. When you read let x = 5, you know that x will always be 5 in that scope. No surprises.

Opting Into Mutability

When you need mutation, add mut:

let mut x = 5;
x = 6; // Works fine

The mut keyword is a signal to both the compiler and future readers: "This value will change."

// Challenge

Mutability Check

What keyword makes a variable mutable in Rust?

$

Constants

For values that should never change and are known at compile time, use const:

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Constants differ from immutable variables in important ways:

  • They require a type annotation
  • They can only be set to a constant expression (no runtime computation)
  • They're valid for the entire program lifetime in the scope where declared
  • Convention: SCREAMING_SNAKE_CASE
// Tip-When to use const

Use const for truly constant values like configuration, mathematical constants, or magic numbers. Use let for values computed at runtime, even if they won't change.

Shadowing

Rust lets you declare a new variable with the same name as a previous one. The new variable "shadows" the old:

fn main() {
    let x = 5;
    let x = x + 1;  // x is now 6

    {
        let x = x * 2;
        println!("Inner scope: {x}"); // Prints 12
    }

    println!("Outer scope: {x}"); // Prints 6
}

When the inner scope ends, the shadowing ends. The original x (value 6) comes back into view.

Shadowing differs from mut in a crucial way: you can change the type.

let spaces = "   ";         // &str
let spaces = spaces.len();  // usize - same name, different type

With mut, the compiler would reject this type change. Shadowing gives you flexibility while still requiring explicit intent at each reassignment.

// Warning-Shadowing vs Mutation

Shadowing creates a new variable. The old value still exists until dropped. With mut, you're modifying the same memory location. Choose shadowing when transforming a value; choose mut when updating a value of the same type repeatedly.

Scalar Types

Scalar types represent a single value. Rust has four: integers, floating-point numbers, booleans, and characters.

Integers

Rust's integer types are explicit about size and signedness:

SignedUnsignedSize
i8u88-bit
i16u1616-bit
i32u3232-bit
i64u6464-bit
i128u128128-bit
isizeusizearch-dependent

Signed types (i) can hold negative values. Unsigned types (u) are always positive or zero.

The isize and usize types depend on your architecture: 64 bits on a 64-bit system, 32 bits on a 32-bit system. They're primarily used for indexing collections.

// Tip-Default Integer Type

When you write let x = 5; without a type annotation, Rust defaults to i32. It's fast on most systems, even 64-bit ones.

Integer Literals

Rust offers multiple ways to write integer literals:

FormatExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

Notice the underscores. They're visual separators that make large numbers readable. 1_000_000 is easier to parse than 1000000.

Floating-Point Numbers

Rust has two floating-point types: f32 and f64. Both are signed.

let x = 2.0;      // f64 (default)
let y: f32 = 3.0; // f32

The default is f64 because on modern CPUs it's roughly the same speed as f32 but offers more precision.

Booleans

let t = true;
let f: bool = false;

Booleans are one byte in size. Nothing surprising here.

Characters

let c = 'z';
let heart = '♥';
let emoji = '🦀';

Rust's char type is four bytes and represents a Unicode Scalar Value. It can hold much more than ASCII: accented letters, emojis, Chinese characters, zero-width spaces. Single quotes, not double.

// Info-char vs String

A char is a single Unicode scalar value. A String is a sequence of UTF-8 encoded bytes. We'll dive into strings when we cover ownership and collections.

Compound Types

Compound types group multiple values. Rust has two primitives: tuples and arrays.

Tuples

Tuples combine values of different types into one compound:

let tup: (i32, f64, u8) = (500, 6.4, 1);

You can destructure to extract values:

let (x, y, z) = tup;
println!("y is {y}"); // 6.4

Or access by index:

let first = tup.0;  // 500
let second = tup.1; // 6.4

Tuple indices are zero-based and use dot notation, not brackets.

A tuple without values () is called the "unit type." It represents an empty value or empty return type. Functions without a return type implicitly return ().

Arrays

Arrays hold multiple values of the same type with a fixed length:

let a = [1, 2, 3, 4, 5];

Arrays live on the stack because their size is known at compile time. For dynamic-length collections, you'll use Vec (covered later).

Type annotation includes both the element type and length:

let a: [i32; 5] = [1, 2, 3, 4, 5];

To create an array filled with the same value:

let zeros = [0; 5]; // [0, 0, 0, 0, 0]

Access elements with bracket notation:

let first = a[0];
let second = a[1];
// Danger-Bounds Checking

Rust checks array bounds at runtime. Accessing an invalid index causes a panic, not undefined behavior. This is a safety feature. In C, out-of-bounds access could read arbitrary memory.

Rust is Expression-Based

Here's something that might surprise you: almost everything in Rust is an expression that returns a value.

The difference between statements and expressions:

  • Statements perform an action but don't return a value
  • Expressions evaluate to a value
let y = {
    let x = 3;
    x + 1  // No semicolon - this is an expression
};

println!("{y}"); // 4

Notice x + 1 has no semicolon. Adding a semicolon turns an expression into a statement that returns ().

This means if can return a value:

let number = if condition { 5 } else { 6 };

We'll explore this more when we cover functions and control flow.

// Challenge

Type Detective

Given `let x = 5;` without a type annotation, what type does Rust infer for x?

$

Summary

Rust's type system is explicit and intentional:

  • Immutable by default - use mut when you need to change values
  • Shadowing - reuse variable names, even with different types
  • Explicit integer sizes - no ambiguity about representation
  • Expression-based - blocks and control flow return values

This intentionality extends throughout the language. In the next lesson, we'll see how functions and control flow leverage Rust's expression-based nature to write clean, concise code.

<-[prev]Hello, Rust[next]->Functions and Control Flow
Progress: 0%
// On this page
  • Why Immutability by Default?
  • Opting Into Mutability
  • Constants
  • Shadowing
  • Scalar Types
  • Integers
  • Integer Literals
  • Floating-Point Numbers
  • Booleans
  • Characters
  • Compound Types
  • Tuples
  • Arrays
  • Rust is Expression-Based
  • Summary