RUST: Variables and Types in Rust

Rust variables are immutable by default, a design decision that eliminates a vast category of bugs. Understanding Rust's type system — from primitive integers to floating-point numbers, booleans, and characters — is the foundation for writing correct and efficient Rust programs.

Variables in Rust behave differently from most other programming languages. By default, every variable you create is immutable — you cannot change it after binding a value to it. This is not a limitation; it is a deliberate design choice that helps the compiler catch bugs and enables optimizations.


Declaring Variables with `let`

In Rust, variables are declared using the let keyword:

rust
let x = 5;

Attempting to reassign an immutable variable is a compile-time error:

rust
let x = 5;
x = 10; // error: cannot assign twice to immutable variable

To make a variable mutable, add the mut keyword:

rust
let mut x = 5;
x = 10; // OK

This design principle — immutability by default — forces you to think carefully about which data in your program actually needs to change. In many codebases, the majority of variables should be immutable.


Type Annotations

Rust is a statically typed language, meaning every variable has a known type at compile time. Rust can often infer the type automatically, but you can also annotate it explicitly:

rust
let a: u8 = 123;   // explicit type annotation
let b = 123456789; // compiler infers i32

When the type cannot be inferred, Rust requires an explicit annotation. It is good practice to annotate types when writing library code or when the inferred type may be surprising.


Integer Types

Rust provides a rich set of integer types covering both signed and unsigned values across multiple sizes:

TypeWidthRange
i88 bits-128 to 127
i1616 bits-32,768 to 32,767
i3232 bits-2,147,483,648 to 2,147,483,647
i6464 bitsVery large signed range
i128128 bitsEnormous signed range
u88 bits0 to 255
u1616 bits0 to 65,535
u3232 bits0 to 4,294,967,295
u6464 bitsVery large unsigned range
u128128 bitsEnormous unsigned range

rust
let a: u8 = 123;   // 8-bit unsigned — range 0..255
println!("a = {}", a);

let mut b: i8 = 0;
b = 42;
println!("b = {}", b);

The default integer type when not specified is i32, which is generally the fastest integer type on most architectures:

rust
let c = 123456789; // inferred as i32

Checking the Size of a Variable

You can use std::mem::size_of_val to inspect the memory footprint of any variable at runtime:

rust
use std::mem;

let mut a = 123456789;
println!("a = {}, size = {} bytes", a, mem::size_of_val(&a));
// a = 123456789, size = 4 bytes (i32)


Architecture-Sized Integers: `isize` and `usize`

Rust provides two special integer types that adapt to the word size of the target architecture:

  • isize: Signed, pointer-sized integer
  • usize: Unsigned, pointer-sized integer

On a 32-bit system, these are 32 bits. On a 64-bit system, they are 64 bits. The most important use of usize is as an array index, since arrays in Rust are indexed by usize:

rust
use std::mem;

let z: isize = 123;
let size_of_z = mem::size_of_val(&z);
println!("z = {}, takes up {} bytes, {}-bit OS", z, size_of_z, size_of_z * 8);

let idx: usize = 0;
let arr = [10, 20, 30];
println!("arr[0] = {}", arr[idx]);
// arr[0] = 10


Floating-Point Types

Rust provides two floating-point types following the IEEE 754 standard:

TypeWidthPrecision
f3232 bitsSingle precision
f6464 bitsDouble precision

The default floating-point type is f64 because on modern CPUs it is roughly the same speed as f32 while offering twice the precision:

rust
let x: f32 = 3.14;  // single precision
let y = 2.5;        // inferred as f64


The Boolean Type

Rust's boolean type is bool with exactly two values: true and false. It occupies one byte in memory:

rust
let is_active = true;
let is_done: bool = false;
let g = false;
println!("{}", g);  // false

let positive = 5 > 0;
println!("{}", positive); // true

Booleans are the foundation of all conditional logic in Rust.


The Character Type

The char type in Rust represents a single Unicode Scalar Value. Unlike C where char is one byte, Rust's char is four bytes (32 bits) to accommodate the full Unicode character set:

rust
let letter: char = 'a';
let emoji: char = '';
let chinese: char = '';

text
char in C:   1 byte  (ASCII only, 128 characters)
char in Rust: 4 bytes (Unicode, 1,114,112 characters)


Type Inference

Rust's compiler performs type inference, meaning you often do not need to write the type explicitly. The compiler deduces the type from the context:

rust
let x = 5;       // i32
let y = 5.0;     // f64
let z = true;    // bool
let c = 'R';     // char

However, type inference has limits. When the compiler cannot determine the type unambiguously, it will ask you to annotate it:

rust
let v = Vec::new(); // error: type annotations needed
let v: Vec<i32> = Vec::new(); // OK


Integer Literals and Readability

Rust allows underscores in numeric literals to improve readability. This does not affect the value:

rust
let million = 1_000_000;       // same as 1000000
let hex     = 0xFF_AA_BB;      // hexadecimal
let binary  = 0b1111_0000;     // binary
let octal   = 0o77;            // octal
let byte    = b'A';            // byte literal (u8)

You can also suffix literals directly with their type:

rust
let x = 42u8;    // u8
let y = 3.14f32; // f32


Integer Overflow

In debug mode, integer overflow causes a panic (runtime crash) in Rust. In release mode (--release), it wraps around silently. This means overflow bugs are caught during development and testing.

If you intentionally want wrapping arithmetic, Rust provides explicit methods:

rust
let x: u8 = 255;
let y = x.wrapping_add(1); // y = 0 (wraps around)
let z = x.saturating_add(1); // z = 255 (saturates at max)
let w = x.checked_add(1); // w = None (overflow detected)


Summary of Primitive Types

text
Primitive Types in Rust:
+------------------+--------+----------------------------+
| Type             | Size   | Values                     |
+------------------+--------+----------------------------+
| i8 / u8          | 1 byte | signed / unsigned 8-bit    |
| i16 / u16        | 2 bytes| signed / unsigned 16-bit   |
| i32 / u32        | 4 bytes| signed / unsigned 32-bit   |
| i64 / u64        | 8 bytes| signed / unsigned 64-bit   |
| i128 / u128      | 16 bytes| signed / unsigned 128-bit |
| isize / usize    | arch   | pointer-sized integer      |
| f32              | 4 bytes| single-precision float     |
| f64              | 8 bytes| double-precision float     |
| bool             | 1 byte | true / false               |
| char             | 4 bytes| Unicode scalar value       |
+------------------+--------+----------------------------+


A Complete Example

rust
use std::mem;

fn main() {
    // Immutable integer
    let a: u8 = 123;
    println!("a = {}, size = {} bytes", a, mem::size_of_val(&a));

    // Mutable integer
    let mut b: i8 = 0;
    b = 42;
    println!("b = {}", b);

    // Architecture-sized integer
    let z: isize = 123;
    let size_of_z = mem::size_of_val(&z);
    println!("z = {}, takes up {} bytes, {}-bit OS", z, size_of_z, size_of_z * 8);

    // Array indexing with usize
    let idx: usize = 0;
    let arr = [1, 2, 3];
    println!("arr[{}] = {}", idx, arr[idx]);

    // Character
    let d: char = 'a';
    println!("d = {}, size = {} bytes", d, mem::size_of_val(&d));

    // Float
    let e = 2.5f64;
    println!("e = {}", e);

    // Boolean
    let g = false;
    println!("g = {}", g);
    let h = 5 > 0;
    println!("h = {}", h);
}


Conclusion

Rust's type system is one of its greatest strengths. Immutability by default eliminates accidental mutation bugs. The rich set of integer types lets you express intent precisely — use u8 for a byte, i32 for a general integer, usize for an array index. The compiler's type inference means you rarely need to write types explicitly, but when you do, the annotations become living documentation.

Understanding these fundamentals is essential for everything that follows in Rust, from ownership and borrowing to generic programming and concurrent code.