Data Types

Rust is statically typed - all variable types must be known at compile time. The compiler can usually infer types, but explicit annotations are required when multiple types are possible:

let guess: u32 = "42".parse().expect("Not a number!");

Without the type annotation:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
  |
2 |     let guess: /* Type */ = "42".parse().expect("Not a number!");
  |              ++++++++++++

For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error

Scalar Types

Rust has four primary scalar types: integers, floating-point numbers, booleans, and characters.

Integer Types

Table 3-1: Integer Types in Rust

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Signed variants store numbers from −(2n − 1) to 2n − 1 − 1, unsigned from 0 to 2n − 1. isize and usize depend on target architecture (64-bit on 64-bit systems).

Integer literals:

Table 3-2: Integer Literals in Rust

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

Default integer type is i32. Use isize/usize for collection indexing.

Integer Overflow

In debug mode, integer overflow causes panics. In release mode (--release), Rust performs two’s complement wrapping. Handle overflow explicitly with:

  • wrapping_* methods (e.g., wrapping_add)
  • checked_* methods (return Option)
  • overflowing_* methods (return value + overflow boolean)
  • saturating_* methods (clamp to min/max)

Floating-Point Types

Two floating-point types: f32 and f64 (default). Both are signed and follow IEEE-754 standard.

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Numeric Operations

Standard mathematical operations: +, -, *, /, %. Integer division truncates toward zero.

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

Boolean Type

bool type with values true and false. One byte in size.

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

Character Type

char type represents Unicode scalar values (4 bytes). Use single quotes for char literals, double quotes for strings.

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

Valid Unicode scalar values: U+0000 to U+D7FF and U+E000 to U+10FFFF.

Compound Types

Tuple Type

Fixed-length collection of values with heterogeneous types:

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Access elements by destructuring:

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

Or by index:

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

Empty tuple () is called unit and represents an empty value or return type.

Array Type

Fixed-length collection of homogeneous types, allocated on the stack:

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Type annotation syntax:

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

Initialize with repeated values:

let a = [3; 5]; // [3, 3, 3, 3, 3]

Use Vec<T> for dynamic arrays. Arrays are better when size is known at compile time.

Array Access
fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

Out-of-bounds access causes runtime panic:

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

Output:

thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Rust prevents buffer overflows by bounds checking at runtime.