1. rust
  2. /basics
  3. /data-types

Data Types

Rust is a statically typed language, meaning the type of every variable must be known at compile time. Rust's type system is designed to prevent common programming errors while providing zero-cost abstractions and excellent performance.

Type Inference

Rust can often infer types from context, but you can also explicitly annotate types:

fn main() {
    let x = 5;          // Type inferred as i32
    let y: i32 = 5;     // Explicit type annotation
    let z: f64 = 3.14;  // Must specify for floating-point disambiguation
}

Scalar Types

Scalar types represent single values. Rust has four primary scalar types:

Integer Types

Rust provides several integer types with different sizes and signedness:

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize
fn main() {
    // Signed integers (can be negative)
    let a: i8 = -128;      // Range: -128 to 127
    let b: i32 = -2_147_483_648; // Default integer type
    let c: i64 = 9_223_372_036_854_775_807;
    
    // Unsigned integers (only positive)
    let d: u8 = 255;       // Range: 0 to 255
    let e: u32 = 4_294_967_295;
    let f: u64 = 18_446_744_073_709_551_615;
    
    // Architecture-dependent
    let g: isize = 100;    // Size depends on architecture (32 or 64 bit)
    let h: usize = 200;    // Commonly used for indexing
}

Integer Literals

You can write integer literals in multiple ways:

fn main() {
    let decimal = 98_222;
    let hex = 0xff;
    let octal = 0o77;
    let binary = 0b1111_0000;
    let byte = b'A';  // u8 only
    
    // Type suffixes
    let x = 57u8;
    let y = 100_i32;
    let z = 0xff_u8;
}

Integer Overflow

Rust checks for integer overflow in debug mode:

fn main() {
    let mut x: u8 = 255;
    // x = x + 1; // This would panic in debug mode
    
    // Use wrapping methods to handle overflow explicitly
    x = x.wrapping_add(1); // Results in 0
    println!("Wrapped: {}", x);
    
    // Other overflow methods
    let y: u8 = 250;
    println!("Saturating add: {}", y.saturating_add(10)); // 255
    println!("Checked add: {:?}", y.checked_add(10)); // Some(255) or None
    println!("Overflowing add: {:?}", y.overflowing_add(10)); // (255, true)
}

Floating-Point Types

Rust has two floating-point types:

fn main() {
    let x = 2.0;        // f64 (default)
    let y: f32 = 3.0;   // f32
    
    // Scientific notation
    let large = 1e6;    // 1,000,000.0
    let small = 1e-6;   // 0.000001
    
    // Special values
    let inf = f64::INFINITY;
    let neg_inf = f64::NEG_INFINITY;
    let nan = f64::NAN;
    
    println!("Is NaN: {}", nan.is_nan());
    println!("Is infinite: {}", inf.is_infinite());
}

Boolean Type

The boolean type has two values: true and false:

fn main() {
    let t = true;
    let f: bool = false; // Explicit type annotation
    
    // Boolean operations
    let and = t && f;    // false
    let or = t || f;     // true
    let not = !t;        // false
    
    // Booleans are 1 byte in size
    println!("Size of bool: {}", std::mem::size_of::<bool>());
}

Character Type

Rust's char type represents a Unicode scalar value:

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // Unicode
    let heart_eyed_cat = '😻';
    
    // Characters are 4 bytes (Unicode scalar value)
    println!("Size of char: {}", std::mem::size_of::<char>());
    
    // Character methods
    println!("Is alphabetic: {}", c.is_alphabetic());
    println!("Is numeric: {}", '5'.is_numeric());
    println!("To uppercase: {}", c.to_uppercase().collect::<String>());
}

Compound Types

Compound types group multiple values into one type.

Tuple Type

Tuples group together values of different types:

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
    
    // Destructuring
    let (x, y, z) = tup;
    println!("x: {}, y: {}, z: {}", x, y, z);
    
    // Accessing by index
    let five_hundred = tup.0;
    let six_point_four = tup.1;
    let one = tup.2;
    
    // Empty tuple (unit type)
    let unit: () = ();
    
    // Single element tuple (note the comma)
    let single = (42,);
    
    // Nested tuples
    let nested = ((1, 2), (3, 4));
    println!("Nested access: {}", nested.0.1); // 2
}

Array Type

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

fn main() {
    // Array with explicit type and length
    let a: [i32; 5] = [1, 2, 3, 4, 5];
    
    // Array with repeated values
    let b = [3; 5]; // [3, 3, 3, 3, 3]
    
    // Accessing array elements
    let first = a[0];
    let second = a[1];
    
    // Array length
    println!("Array length: {}", a.len());
    
    // Iterating over arrays
    for element in a.iter() {
        println!("Value: {}", element);
    }
    
    // Array slicing
    let slice = &a[1..3]; // [2, 3]
    println!("Slice: {:?}", slice);
}

String Types

Rust has several string-related types:

String Literals (&str)

fn main() {
    let s = "Hello, world!"; // &str (string slice)
    let s: &str = "Hello, world!"; // Explicit type
    
    // String slice methods
    println!("Length: {}", s.len());
    println!("Is empty: {}", s.is_empty());
    println!("Contains 'world': {}", s.contains("world"));
}

Owned Strings (String)

fn main() {
    let mut s = String::new(); // Empty string
    s.push_str("Hello");
    s.push(' ');
    s.push_str("world!");
    
    // From string literal
    let s2 = String::from("Hello, world!");
    let s3 = "Hello, world!".to_string();
    
    // String methods
    println!("Capacity: {}", s.capacity());
    println!("Length: {}", s.len());
    
    // String concatenation
    let s4 = s + &s2; // s is moved here
    // println!("{}", s); // Error: s has been moved
}

Option and Result Types

These are fundamental enum types for handling nullable values and errors:

Option Type

fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;
    
    // Pattern matching with Option
    match some_number {
        Some(value) => println!("Got a value: {}", value),
        None => println!("Got nothing"),
    }
    
    // Option methods
    let x = Some(5);
    println!("Is some: {}", x.is_some());
    println!("Is none: {}", x.is_none());
    println!("Unwrap or default: {}", x.unwrap_or(0));
}

Result Type

fn main() {
    let success: Result<i32, &str> = Ok(42);
    let failure: Result<i32, &str> = Err("Something went wrong");
    
    // Pattern matching with Result
    match success {
        Ok(value) => println!("Success: {}", value),
        Err(error) => println!("Error: {}", error),
    }
    
    // Result methods
    println!("Is ok: {}", success.is_ok());
    println!("Is err: {}", failure.is_err());
    println!("Unwrap or default: {}", failure.unwrap_or(0));
}

Type Aliases

You can create type aliases for complex or frequently used types:

type Kilometers = i32;
type Point = (i32, i32);
type Result<T> = std::result::Result<T, std::io::Error>;

fn main() {
    let distance: Kilometers = 100;
    let origin: Point = (0, 0);
    
    let file_result: Result<String> = Ok("file contents".to_string());
}

Type Conversion

Rust doesn't perform implicit type conversions, but provides explicit conversion methods:

Casting with as

fn main() {
    let x = 42i32;
    let y = x as f64; // Explicit cast
    let z = x as u8;  // Truncation may occur
    
    // Be careful with casting - data loss can occur
    let large = 300i32;
    let small = large as u8; // Results in 44 (300 - 256)
    println!("Truncated: {}", small);
}

Using From and Into Traits

fn main() {
    // From trait
    let s = String::from("hello");
    let num = i32::from(5u8);
    
    // Into trait (usually inferred)
    let s2: String = "hello".into();
    let num2: i32 = 5u8.into();
    
    // TryFrom for fallible conversions
    use std::convert::TryFrom;
    
    let result = i32::try_from(300u16); // Ok(300)
    let error = i32::try_from(std::u16::MAX); // Still Ok for u16 to i32
    
    match u8::try_from(300i32) {
        Ok(value) => println!("Converted: {}", value),
        Err(e) => println!("Conversion failed: {}", e),
    }
}

Custom Types

You can define your own types using structs and enums:

Structs

struct Point {
    x: i32,
    y: i32,
}

struct Color(i32, i32, i32); // Tuple struct

struct UnitStruct; // Unit struct

fn main() {
    let p = Point { x: 10, y: 20 };
    let red = Color(255, 0, 0);
    let unit = UnitStruct;
    
    println!("Point: ({}, {})", p.x, p.y);
    println!("Red component: {}", red.0);
}

Enums

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::Write(String::from("hello"));
    
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Text: {}", text),
        Message::ChangeColor(r, g, b) => println!("Color: ({}, {}, {})", r, g, b),
    }
}

Generic Types

Rust supports generic types for writing flexible, reusable code:

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    
    largest
}

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    let result = largest(&numbers);
    println!("The largest number is {}", result);
    
    let integer_point = Point::new(5, 10);
    let float_point = Point::new(1.0, 4.0);
}

Memory Layout and Size

Understanding type sizes can be important for performance:

fn main() {
    println!("Size of i8: {}", std::mem::size_of::<i8>());     // 1
    println!("Size of i32: {}", std::mem::size_of::<i32>());   // 4
    println!("Size of i64: {}", std::mem::size_of::<i64>());   // 8
    println!("Size of f64: {}", std::mem::size_of::<f64>());   // 8
    println!("Size of bool: {}", std::mem::size_of::<bool>()); // 1
    println!("Size of char: {}", std::mem::size_of::<char>()); // 4
    
    // Compound types
    println!("Size of (i32, i32): {}", std::mem::size_of::<(i32, i32)>()); // 8
    println!("Size of [i32; 5]: {}", std::mem::size_of::<[i32; 5]>());     // 20
    
    // Reference types
    println!("Size of &i32: {}", std::mem::size_of::<&i32>());     // 8 on 64-bit
    println!("Size of &str: {}", std::mem::size_of::<&str>());     // 16 (ptr + len)
    println!("Size of String: {}", std::mem::size_of::<String>()); // 24 (ptr + len + cap)
}

Understanding Rust's type system is fundamental to writing safe and efficient Rust code. The type system prevents many common programming errors at compile time while providing excellent performance through zero-cost abstractions.