Module 1 • Lesson 5

Data Types in Rust

📚 9 min read 💻 Free Course 🦀 nixus.pro

Rust's Type System: Strict but Smart

Rust is statically typed - every value has a type known at compile time. But thanks to type inference, you rarely need to annotate types explicitly. When you write let x = 5, Rust infers x is an integer. When ambiguous, you annotate.

Integer Types

fn main() {
    // Signed integers (i8, i16, i32, i64, i128, isize)
    let a: i8  = 127;        // -128 to 127
    let b: i16 = 32_767;     // -32768 to 32767
    let c: i32 = 2_147_483_647;  // Default integer type
    let d: i64 = 9_223_372_036_854_775_807;
    let e: i128 = 170_141_183_460_469_231_731_687_303_715_884_105_727;

    // Unsigned integers (u8, u16, u32, u64, u128, usize)
    let f: u8  = 255;        // 0 to 255
    let g: u32 = 4_294_967_295;
    let h: usize = 1024;     // Platform-dependent (pointer size)

    // Integer literals
    let decimal     = 98_222;    // Decimal
    let hex         = 0xff;      // Hex
    let octal       = 0o77;      // Octal
    let binary      = 0b1111_0000; // Binary
    let byte        = b'A';      // Byte (u8 only)

    println!("{} {} {} {} {}", a, b, c, d, h);
}
Integer Overflow

In debug mode, integer overflow causes a panic (crash). In release mode, it wraps around silently. Use checked arithmetic (checked_add, saturating_add, wrapping_add) for explicit overflow handling.

Floating Point and Boolean

fn main() {
    // Floating point: f32 and f64
    let x: f64 = 2.0;       // Default float type
    let y: f32 = 3.0;       // 32-bit (less precise)

    // Floating point operations
    let sum       = x + y as f64;  // as for casting
    let difference = x - 1.0;
    let product   = x * 3.0;
    let quotient  = x / y as f64;
    let remainder = x % 3.0;

    println!("{} {} {} {} {}", sum, difference, product, quotient, remainder);

    // Special float values
    let infinity = f64::INFINITY;
    let neg_infinity = f64::NEG_INFINITY;
    let nan = f64::NAN;
    println!("{} {} {}", infinity, neg_infinity, nan);
    println!("Is NaN: {}", nan.is_nan());

    // Boolean
    let t: bool = true;
    let f: bool = false;
    println!("{} {}", t, f);
    println!("AND: {}", t && f);
    println!("OR: {}", t || f);
    println!("NOT: {}", !t);
}

Characters and Text

fn main() {
    // char is 4 bytes - a Unicode scalar value
    let c: char = 'z';
    let emoji: char = '😂';
    let heart: char = '❤';
    let japanese: char = 'こ';

    println!("{} {} {} {}", c, emoji, heart, japanese);
    println!("Is alphabetic: {}", c.is_alphabetic());
    println!("Is digit: {}", '5'.is_ascii_digit());
    println!("To uppercase: {}", 'a'.to_uppercase().next().unwrap());

    // char operations
    println!("ASCII value of 'A': {}", 'A' as u8);  // 65
    println!("Char from 66: {}", 66u8 as char);     // 'B'
}

Compound Types: Tuples and Arrays

fn main() {
    // Tuples: fixed-length, heterogeneous
    let tup: (i32, f64, bool, char) = (500, 6.4, true, 'z');

    // Access by index
    println!("First: {}", tup.0);
    println!("Second: {}", tup.1);

    // Destructuring
    let (x, y, flag, letter) = tup;
    println!("{} {} {} {}", x, y, flag, letter);

    // Unit tuple - empty tuple, used as "void"
    let unit: () = ();

    // Arrays: fixed-length, homogeneous
    let arr: [i32; 5] = [1, 2, 3, 4, 5];
    println!("Array: {:?}", arr);
    println!("Length: {}", arr.len());
    println!("First: {}", arr[0]);

    // Initialize with same value
    let zeros = [0; 10];  // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    println!("{:?}", zeros);

    // Slices of arrays
    let slice = &arr[1..3];  // [2, 3]
    println!("{:?}", slice);

    // Iteration
    for element in &arr {
        print!("{} ", element);
    }
    println!();
}
Arrays vs Vectors

Arrays in Rust have a fixed size known at compile time and live on the stack. For dynamic collections that can grow, use Vec<T> - covered in Module 5. Use arrays when the size is always known and small.

Type Casting

fn main() {
    // Use 'as' for numeric casting
    let x: i32 = 1000;
    let y = x as u8;  // Truncates! 1000 % 256 = 232
    println!("{}", y);  // 232, not 1000!

    let f: f64 = 3.9;
    let i = f as i32;  // Truncates, does not round
    println!("{}", i);  // 3

    // Safe conversions with From/Into
    let a: i32 = 5;
    let b: i64 = i64::from(a);   // i32 always fits in i64
    let c: i64 = a.into();       // Same thing, different syntax

    // Fallible conversions with TryFrom/TryInto
    use std::convert::TryFrom;
    let big: i64 = 1000;
    let result = i8::try_from(big);  // Returns Result
    match result {
        Ok(val) => println!("Fits: {}", val),
        Err(e)  => println!("Does not fit: {}", e),
    }
}

🎯 Practice Exercise

  1. Create variables of at least 6 different types and print them
  2. Create a tuple holding (name: &str, age: u8, height_cm: f32) and destructure it
  3. Create an array of 5 temperatures (f64), iterate over it, and print the average
  4. Demonstrate integer overflow by intentionally creating one in debug mode (use checked_add to handle it safely)

🎉 Key Takeaways