Rust - Data Types

Overview

Estimated time: 40–50 minutes

Master Rust's type system, including scalar types (integers, floats, booleans, characters) and compound types (tuples, arrays). Learn type inference, explicit type annotations, and how Rust's type system ensures memory safety and performance.

Learning Objectives

Prerequisites

Rust's Type System

Rust is a statically typed language, meaning all variable types must be known at compile time. However, Rust has powerful type inference that can figure out types for you in most cases.

Type Inference vs Explicit Types

fn main() {
    // Type inference - Rust figures out the type
    let guess = 42;        // Rust infers i32
    let pi = 3.14;         // Rust infers f64
    let is_ready = true;   // Rust infers bool
    
    // Explicit type annotations
    let age: u8 = 25;      // Explicitly u8
    let price: f32 = 19.99; // Explicitly f32
    let name: &str = "Alice"; // Explicitly &str
    
    println!("guess: {}, pi: {}, ready: {}", guess, pi, is_ready);
    println!("age: {}, price: {}, name: {}", age, price, name);
}

Expected Output:

guess: 42, pi: 3.14, ready: true
age: 25, price: 19.99, name: Alice

Scalar Types

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

Integer Types

Signed Integers

Type Size Range
i8 8 bits -128 to 127
i16 16 bits -32,768 to 32,767
i32 32 bits -2,147,483,648 to 2,147,483,647
i64 64 bits -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
i128 128 bits Very large range
isize arch Depends on architecture (32-bit or 64-bit)

Unsigned Integers

Type Size Range
u8 8 bits 0 to 255
u16 16 bits 0 to 65,535
u32 32 bits 0 to 4,294,967,295
u64 64 bits 0 to 18,446,744,073,709,551,615
u128 128 bits Very large range
usize arch Depends on architecture

Integer Examples

fn main() {
    // Different integer types
    let small: i8 = 127;
    let medium: i32 = 1_000_000;  // Underscores for readability
    let large: i64 = 9_223_372_036_854_775_807;
    
    let byte: u8 = 255;
    let unsigned: u32 = 4_294_967_295;
    
    // Architecture dependent
    let pointer_sized: usize = 100;
    
    println!("small: {}, medium: {}, large: {}", small, medium, large);
    println!("byte: {}, unsigned: {}, pointer_sized: {}", byte, unsigned, pointer_sized);
    
    // Integer literals with suffixes
    let decimal = 98_222u32;       // u32
    let hex = 0xffu8;              // u8, hex format
    let octal = 0o77i16;           // i16, octal format
    let binary = 0b1111_0000u8;    // u8, binary format
    let byte_literal = b'A';       // u8, byte literal
    
    println!("decimal: {}, hex: {}, octal: {}, binary: {}, byte: {}", 
             decimal, hex, octal, binary, byte_literal);
}

Expected Output:

small: 127, medium: 1000000, large: 9223372036854775807
byte: 255, unsigned: 4294967295, pointer_sized: 100
decimal: 98222, hex: 255, octal: 63, binary: 240, byte: 65

Floating-Point Types

Rust has two floating-point types: f32 (32-bit) and f64 (64-bit). The default is f64.

fn main() {
    let x = 2.0;        // f64 (default)
    let y: f32 = 3.0;   // f32
    
    // Floating-point operations
    let sum = x + y as f64;  // Need to cast f32 to f64
    let difference = x - 1.5;
    let product = y * 2.5;
    let quotient = x / 3.0;
    
    println!("x: {}, y: {}", x, y);
    println!("sum: {}, difference: {}, product: {}, quotient: {}", 
             sum, difference, product, quotient);
    
    // Scientific notation
    let large_float = 1.23e10;     // 12,300,000,000
    let small_float = 1.23e-4;     // 0.000123
    
    println!("large: {}, small: {}", large_float, small_float);
    
    // Special float values
    let infinity = f64::INFINITY;
    let neg_infinity = f64::NEG_INFINITY;
    let not_a_number = f64::NAN;
    
    println!("infinity: {}, neg_infinity: {}, nan: {}", 
             infinity, neg_infinity, not_a_number);
}

Expected Output:

x: 2, y: 3
sum: 5, difference: 0.5, product: 7.5, quotient: 0.6666666666666666
large: 12300000000, small: 0.000123
infinity: inf, neg_infinity: -inf, nan: NaN

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_result = t && f;    // false
    let or_result = t || f;     // true
    let not_result = !t;        // false
    
    println!("t: {}, f: {}", t, f);
    println!("AND: {}, OR: {}, NOT: {}", and_result, or_result, not_result);
    
    // Booleans in conditions
    if t {
        println!("t is true!");
    }
    
    // Boolean from comparisons
    let is_equal = 5 == 5;
    let is_greater = 10 > 5;
    let is_not_equal = 3 != 4;
    
    println!("is_equal: {}, is_greater: {}, is_not_equal: {}", 
             is_equal, is_greater, is_not_equal);
}

Expected Output:

t: true, f: false
AND: false, OR: true, NOT: false
t is true!
is_equal: true, is_greater: true, is_not_equal: true

Character Type

Rust's char type represents a Unicode scalar value and is 4 bytes in size.

fn main() {
    let c = 'z';
    let z: char = 'ℤ';  // Unicode character
    let heart_eyed_cat = '😻';
    
    println!("c: {}, z: {}, cat: {}", c, z, heart_eyed_cat);
    
    // More Unicode examples
    let chinese = '中';
    let arabic = 'ع';
    let musical_note = '♪';
    
    println!("Chinese: {}, Arabic: {}, Note: {}", chinese, arabic, musical_note);
    
    // Character properties
    println!("Is '5' a digit? {}", '5'.is_ascii_digit());
    println!("Is 'A' uppercase? {}", 'A'.is_ascii_uppercase());
    println!("Is 'hello' alphabetic? {}", 'h'.is_alphabetic());
    
    // Converting case
    let uppercase_a = 'a'.to_ascii_uppercase();
    let lowercase_z = 'Z'.to_ascii_lowercase();
    
    println!("'a' uppercase: {}, 'Z' lowercase: {}", uppercase_a, lowercase_z);
}

Expected Output:

c: z, z: ℤ, cat: 😻
Chinese: 中, Arabic: ع, Note: ♪
Is '5' a digit? true
Is 'A' uppercase? true
Is 'hello' alphabetic? true
'a' uppercase: A, 'Z' lowercase: z

Compound Types

Compound types group multiple values into one type. Rust has two primitive compound types: tuples and arrays.

Tuples

Tuples group together values of different types. They have a fixed length.

Creating and Accessing Tuples

fn main() {
    // Creating tuples
    let tup: (i32, f64, u8) = (500, 6.4, 1);
    let simple_tup = (42, "hello", true);
    
    // Destructuring tuples
    let (x, y, z) = tup;
    println!("x: {}, y: {}, z: {}", x, y, z);
    
    // Accessing tuple elements by index
    let first = simple_tup.0;   // 42
    let second = simple_tup.1;  // "hello"
    let third = simple_tup.2;   // true
    
    println!("first: {}, second: {}, third: {}", first, second, third);
    
    // Empty tuple (unit type)
    let unit = ();
    println!("Unit type: {:?}", unit);
}

Expected Output:

x: 500, y: 6.4, z: 1
first: 42, second: hello, third: true
Unit type: ()

Tuple Methods and Patterns

fn main() {
    let person = ("Alice", 30, "Engineer");
    
    // Pattern matching with tuples
    match person {
        (name, age, job) => {
            println!("{} is {} years old and works as an {}", name, age, job);
        }
    }
    
    // Ignoring values in destructuring
    let coordinates = (3.14, 2.71, 1.41);
    let (x, _, z) = coordinates;  // Ignore y coordinate
    println!("x: {}, z: {}", x, z);
    
    // Tuples as function return types
    let (quotient, remainder) = divide_with_remainder(17, 5);
    println!("17 ÷ 5 = {} remainder {}", quotient, remainder);
}

fn divide_with_remainder(dividend: i32, divisor: i32) -> (i32, i32) {
    (dividend / divisor, dividend % divisor)
}

Expected Output:

Alice is 30 years old and works as an Engineer
x: 3.14, z: 1.41
17 ÷ 5 = 3 remainder 2

Arrays

Arrays contain multiple values of the same type and have a fixed length known at compile time.

Creating and Accessing Arrays

fn main() {
    // Creating arrays
    let numbers = [1, 2, 3, 4, 5];
    let months = ["January", "February", "March", "April", "May"];
    
    // Array with explicit type and size
    let explicit: [i32; 5] = [1, 2, 3, 4, 5];
    
    // Array with repeated values
    let zeros = [0; 10];  // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    
    println!("numbers: {:?}", numbers);
    println!("months: {:?}", months);
    println!("zeros length: {}", zeros.len());
    
    // Accessing array elements
    let first = numbers[0];
    let second = numbers[1];
    
    println!("first: {}, second: {}", first, second);
    
    // Array slicing
    let slice = &numbers[1..4];  // [2, 3, 4]
    println!("slice: {:?}", slice);
}

Expected Output:

numbers: [1, 2, 3, 4, 5]
months: ["January", "February", "March", "April", "May"]
zeros length: 10
first: 1, second: 2
slice: [2, 3, 4]

Array Methods and Iteration

fn main() {
    let mut numbers = [5, 4, 3, 2, 1];
    
    println!("Original: {:?}", numbers);
    println!("Length: {}", numbers.len());
    
    // Iterating over arrays
    for number in numbers.iter() {
        println!("Value: {}", number);
    }
    
    // Iterating with index
    for (index, value) in numbers.iter().enumerate() {
        println!("Index {}: {}", index, value);
    }
    
    // Modifying array elements
    numbers[0] = 10;
    numbers[4] = 20;
    
    println!("Modified: {:?}", numbers);
    
    // Array methods
    let sum: i32 = numbers.iter().sum();
    let max = numbers.iter().max();
    
    println!("Sum: {}, Max: {:?}", sum, max);
}

Expected Output:

Original: [5, 4, 3, 2, 1]
Length: 5
Value: 5
Value: 4
Value: 3
Value: 2
Value: 1
Index 0: 5
Index 1: 4
Index 2: 3
Index 3: 2
Index 4: 1
Modified: [10, 4, 3, 2, 20]
Sum: 39, Max: Some(20)

Type Conversion and Casting

Explicit Type Conversion

Rust doesn't perform implicit type conversions. You must be explicit:

fn main() {
    let integer = 42i32;
    let float = 3.14f64;
    
    // Explicit casting with 'as'
    let integer_as_float = integer as f64;
    let float_as_integer = float as i32;  // Truncates decimal part
    
    println!("integer: {}, as float: {}", integer, integer_as_float);
    println!("float: {}, as integer: {}", float, float_as_integer);
    
    // Casting between integer types
    let large: i64 = 1000;
    let small: i32 = large as i32;
    
    println!("large: {}, small: {}", large, small);
    
    // Casting can truncate
    let big_number: i32 = 300;
    let small_type: i8 = big_number as i8;  // Truncation occurs!
    
    println!("big_number: {}, truncated: {}", big_number, small_type);
    
    // Character and byte conversions
    let character = 'A';
    let byte = character as u8;
    let back_to_char = byte as char;
    
    println!("char: {}, byte: {}, back: {}", character, byte, back_to_char);
}

Expected Output:

integer: 42, as float: 42
float: 3.14, as integer: 3
large: 1000, small: 1000
big_number: 300, truncated: 44
char: A, byte: 65, back: A

Safe Conversion Methods

fn main() {
    let number: i32 = 42;
    
    // TryFrom/TryInto for safe conversions
    use std::convert::TryInto;
    
    let safe_u8: Result = number.try_into();
    match safe_u8 {
        Ok(value) => println!("Safe conversion: {}", value),
        Err(e) => println!("Conversion failed: {:?}", e),
    }
    
    // Converting strings to numbers
    let string_number = "42";
    let parsed: Result = string_number.parse();
    
    match parsed {
        Ok(n) => println!("Parsed number: {}", n),
        Err(e) => println!("Parse error: {:?}", e),
    }
    
    // More parsing examples
    let float_string = "3.14";
    let float_value: f64 = float_string.parse().unwrap_or(0.0);
    println!("Parsed float: {}", float_value);
    
    // Converting numbers to strings
    let number = 123;
    let number_string = number.to_string();
    println!("Number as string: '{}'", number_string);
}

Expected Output:

Safe conversion: 42
Parsed number: 42
Parsed float: 3.14
Number as string: '123'

Type Aliases

You can create type aliases to make code more readable:

// Type aliases
type Kilometers = i32;
type UserId = u64;
type Point = (f64, f64);

fn main() {
    let distance: Kilometers = 100;
    let user: UserId = 12345;
    let origin: Point = (0.0, 0.0);
    
    println!("Distance: {} km", distance);
    println!("User ID: {}", user);
    println!("Point: {:?}", origin);
    
    // Function using type aliases
    let new_point = create_point(3.5, 4.2);
    let dist = calculate_distance(origin, new_point);
    
    println!("Distance between points: {:.2}", dist);
}

fn create_point(x: f64, y: f64) -> Point {
    (x, y)
}

fn calculate_distance(p1: Point, p2: Point) -> f64 {
    let dx = p2.0 - p1.0;
    let dy = p2.1 - p1.1;
    (dx * dx + dy * dy).sqrt()
}

Expected Output:

Distance: 100 km
User ID: 12345
Point: (0.0, 0.0)
Distance between points: 5.57

Choosing the Right Type

Guidelines for Type Selection

Integers

Floating Point

fn main() {
    // Good type choices
    let age: u8 = 25;              // Age is always positive, fits in u8
    let population: u32 = 8_000_000; // Large positive number
    let balance: f64 = 1234.56;    // Money needs precision
    let array_index: usize = 42;   // Array indices should be usize
    
    // Consider the range and usage
    let temperature: i16 = -40;    // Can be negative, i16 is sufficient
    let file_size: u64 = 1_073_741_824; // File sizes can be very large
    
    println!("age: {}, population: {}, balance: {}", age, population, balance);
    println!("index: {}, temp: {}°C, file_size: {} bytes", 
             array_index, temperature, file_size);
}

Expected Output:

age: 25, population: 8000000, balance: 1234.56
index: 42, temp: -40°C, file_size: 1073741824 bytes

Common Type-Related Errors

Type Mismatch

fn main() {
    let x: i32 = 5;
    let y: i64 = 10;
    
    // This would cause an error:
    // let sum = x + y;  // Error: type mismatch
    
    // Correct approach: cast one type to match the other
    let sum = x as i64 + y;
    println!("Sum: {}", sum);
    
    // Or cast both to a common type
    let sum2 = (x as f64) + (y as f64);
    println!("Sum as f64: {}", sum2);
}

Integer Overflow

fn main() {
    let mut x: u8 = 255;
    
    // In debug mode, this would panic:
    // x += 1;  // Panic: overflow
    
    // Safe ways to handle potential overflow:
    x = x.saturating_add(1);  // Saturates at maximum value
    println!("Saturating add: {}", x);  // 255
    
    let (result, overflowed) = 255u8.overflowing_add(1);
    println!("Overflowing add: {}, overflowed: {}", result, overflowed);  // 0, true
    
    let checked = 255u8.checked_add(1);
    match checked {
        Some(value) => println!("Checked add: {}", value),
        None => println!("Overflow detected!"),
    }
}

Expected Output:

Saturating add: 255
Overflowing add: 0, overflowed: true
Overflow detected!

Best Practices

Type Selection Guidelines

Performance Considerations

Common Pitfalls

Checks for Understanding

  1. What's the default integer type in Rust?
  2. What's the difference between char and u8?
  3. How do you access the second element of a tuple?
  4. What happens when you try to assign an i64 value to an i32 variable?
  5. What's the difference between an array and a tuple?

Answers

  1. i32 - 32-bit signed integer
  2. char is 4 bytes and represents Unicode; u8 is 1 byte and represents raw bytes
  3. Use dot notation with index: tuple.1
  4. Compile error - you must explicitly cast with as i32
  5. Arrays hold multiple values of the same type; tuples hold multiple values of potentially different types

← PreviousNext →