Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 004 / 100

Rust basic types: a practical guide for developers

Scalars, tuples and arrays.

Expona Admin8 min read

Getting Started with Rust's Type System

After three days of setting up your environment and understanding Rust's ownership model, you're ready to work with actual data. Rust's basic types form the foundation of every program you'll write, and understanding them deeply will save you hours of debugging later. This guide covers Rust basic types: a practical guide for developers, focusing on the scalar types, composite structures, and collections you'll reach for daily in production code.

Integer Types: i32, u64, and Friends

Rust gives you explicit control over integer size and signedness. Unlike languages where you guess whether an int is 32 or 64 bits, Rust forces you to choose. The two main categories are signed integers (i8, i16, i32, i64, i128) and unsigned integers (u8, u16, u32, u64, u128).

In most application code, you'll use i32 or i64. The i32 type is Rust's default integer inference type—when the compiler can't determine which integer type you want, it assumes i32. For database IDs, timestamps, or any value that might exceed 2 billion, use i64. Unsigned types like u64 are perfect for counts, array indices, or scenarios where negative numbers make no sense.

Here's a practical example from an e-commerce system:

rust
fn calculate_order_total(
    item_count: u32,
    price_cents: i32,
    discount_cents: i32,
) -> i32 {
    let base_total = (item_count as i32) * price_cents;
    let final_total = base_total - discount_cents;
    
    if final_total < 0 {
        0
    } else {
        final_total
    }
}

fn main() {
    let total = calculate_order_total(5, 1999, 500);
    println!("Order total: ${}.{:02}", total / 100, total % 100);
}

Notice we cast u32 to i32 explicitly. Rust won't let you mix unsigned and signed types without an intentional cast—this prevents subtle bugs where a negative number silently wraps to a huge positive value.

Floating-Point Numbers: f64 for Production

Rust provides f32 and f64 for floating-point arithmetic. Use f64 in production code. It's the default float type, has better precision, and modern processors handle it just as quickly as f32. The only reason to use f32 is when you're storing millions of values and memory is genuinely constrained.

Critical note: never use floating-point for financial calculations. If you're building payment systems, use fixed-point arithmetic or integer-based representations instead. A common pattern is to store amounts in cents as i64 and convert for display only.

rust
fn calculate_moving_average(values: &[f64]) -> f64 {
    if values.is_empty() {
        return 0.0;
    }
    
    let sum: f64 = values.iter().sum();
    sum / values.len() as f64
}

fn main() {
    let metrics = vec![95.5, 92.3, 98.1, 94.7];
    let avg = calculate_moving_average(&metrics);
    println!("Average: {:.2}", avg);
}

Boolean and Character Types

The bool type is straightforward: true or false. Use it for flags, conditions, and return values that signal success or failure. Many Rust codebases use Result<T, E> instead of boolean returns for operations that can fail, which forces callers to handle errors explicitly.

The char type represents a single Unicode scalar value. One character is not necessarily one byte—it's a 4-byte value capable of representing any valid Unicode character. Use char for actual character logic; for text processing and storage, use &str or String.

rust
fn validate_password(password: &str) -> bool {
    let mut has_upper = false;
    let mut has_digit = false;
    let mut has_special = false;
    
    for ch in password.chars() {
        if ch.is_uppercase() {
            has_upper = true;
        } else if ch.is_numeric() {
            has_digit = true;
        } else if !ch.is_alphanumeric() {
            has_special = true;
        }
    }
    
    has_upper && has_digit && has_special && password.len() >= 8
}

fn main() {
    println!("{}", validate_password("Pass123!"));  // true
    println!("{}", validate_password("password"));  // false
}

Tuples: Grouping Mixed Types

Tuples are fixed-size collections that can hold different types. They're immensely useful for returning multiple values from a function or grouping related data without defining a struct. Each position in a tuple has a fixed type that's known at compile time.

Access tuple elements using dot notation with zero-based indexing: my_tuple.0, my_tuple.1, etc. For tuples with named fields, use struct syntax instead—it's clearer and scales better.

rust
fn parse_api_response(status: u16, body: &str) -> (bool, &str) {
    match status {
        200..=299 => (true, body),
        400..=499 => (false, "Client error"),
        500..=599 => (false, "Server error"),
        _ => (false, "Unknown status"),
    }
}

fn process_user_data(name: &str, age: u32) -> (String, bool) {
    let is_adult = age >= 18;
    (name.to_uppercase(), is_adult)
}

fn main() {
    let (success, message) = parse_api_response(200, "{}");
    println!("Success: {}, Message: {}", success, message);
    
    let (upper_name, is_adult) = process_user_data("alice", 25);
    println!("Name: {}, Adult: {}", upper_name, is_adult);
}

Pattern matching on tuples is where they shine. You can destructure them in function parameters, let bindings, and match expressions, making code concise and safe.

Arrays: Fixed-Length Collections

Arrays in Rust have a fixed length known at compile time. The type includes the length: [i32; 5] is different from [i32; 10]. This compile-time guarantee means arrays are allocated on the stack and have zero runtime overhead.

Use arrays when the size is truly fixed and small. For variable-length collections, reach for Vec<T> instead. Arrays are common in low-level code, fixed-size buffers, and situations where you need predictable stack allocation.

rust
fn calculate_percentiles(scores: &[i32; 100]) -> (i32, i32, i32) {
    let mut sorted = scores.clone();
    sorted.sort();
    
    let p25 = sorted[25];
    let p50 = sorted[50];
    let p75 = sorted[75];
    
    (p25, p50, p75)
}

fn main() {
    let mut test_scores: [i32; 5] = [85, 92, 78, 95, 88];
    
    // Iterate and modify
    for score in &mut test_scores {
        *score += 5;  // Curve adjustment
    }
    
    for (i, &score) in test_scores.iter().enumerate() {
        println!("Score {}: {}", i, score);
    }
}

One gotcha: when you return an array from a function or pass it to another function, the size must match exactly. This inflexibility is why Vec<T> is more common in higher-level code.

Vec<T>: Growable Collections

Vec<T> is Rust's growable dynamic array, allocated on the heap. It's the workhorse collection type you'll use constantly. Unlike arrays, vectors can grow and shrink at runtime, and they're homogeneous—all elements must be the same type.

Create vectors with vec! macro, the Vec::new() constructor, or by collecting an iterator. Common operations include push() to add elements, pop() to remove the last element, and indexing with [] or safe access with .get().

rust
fn process_transaction_batch(transactions: Vec) -> (i64, i64, f64) {
    let count = transactions.len() as f64;
    let total: i64 = transactions.iter().sum();
    let max = transactions.iter().max().copied().unwrap_or(0);
    let average = total as f64 / count;
    
    (total, max, average)
}

fn main() {
    // Create and populate a vector
    let mut amounts = vec![1000, 2500, 1800, 3200, 950];
    
    // Add more elements
    amounts.push(2100);
    
    // Safe access with .get()
    if let Some(&last) = amounts.get(amounts.len() - 1) {
        println!("Last amount: {}", last);
    }
    
    let (total, max, avg) = process_transaction_batch(amounts);
    println!("Total: {}, Max: {}, Average: {:.2}", total, max, avg);
    
    // Build a vector from ranges or iterators
    let squares: Vec = (1..=5).map(|x| x * x).collect();
    println!("Squares: {:?}", squares);
}

Vectors move data when they grow beyond their capacity. For performance-critical code, pre-allocate with Vec::with_capacity() if you know the approximate final size.

Common Mistakes

  • Mixing signed and unsigned without explicit casts: Rust won't let you do it, but you'll hit this when combining database IDs (u64) with calculations (i32). Cast intentionally using as.
  • Using floats for money: A classic trap. Decimal rounding errors compound. Store monetary values as integers (cents) and convert for display.
  • Indexing arrays without bounds checks: Direct indexing with [index] panics if out of bounds. Use .get(index) when the index comes from untrusted input.
  • Returning array types from functions: Array types include their length. If you need flexibility, use &[T] (slice) or Vec<T> instead.
  • Cloning vectors unnecessarily: Vectors are moved by default. Only clone when you genuinely need an independent copy. Use references (&Vec<T> or &[T]) to share without copying.

Key Takeaways

  • Choose integer types explicitly: i32 for general use, i64 for large values, u32/u64 for non-negative counts.
  • Always use f64 for floating-point arithmetic in production; never for financial calculations.
  • Tuples are perfect for returning multiple values and destructuring in pattern matching.
  • Arrays have fixed compile-time sizes and live on the stack; use them for small, predictable collections.
  • Vec<T> is your go-to for dynamic, growable collections in most application code.
  • Rust's type system prevents mixing incompatible types, catching entire classes of bugs at compile time rather than in production.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles