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
- Understand Rust's scalar data types and their characteristics.
- Learn about compound types: tuples and arrays.
- Master type inference vs explicit type annotations.
- Understand when and how to choose appropriate types.
- Learn type conversion and casting basics.
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
- i32: Default choice for most integers
- usize/isize: For array indices and collection sizes
- u8: For bytes and small positive numbers (0-255)
- i64/u64: For large numbers
Floating Point
- f64: Default choice, better precision
- f32: When memory usage is critical
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
- Use
i32
as the default integer type unless you need a specific size - Use
f64
as the default floating-point type for better precision - Use
usize
for array indices and collection sizes - Use the smallest type that can hold your data range
- Be explicit with type annotations when clarity is important
Performance Considerations
- Smaller integer types use less memory but may require padding
- Native machine word size (
isize
/usize
) is often fastest f64
is often as fast asf32
on modern hardware- Avoid unnecessary type conversions in hot code paths
Common Pitfalls
- Integer overflow: Be aware of type limits, especially with arithmetic
- Type mismatches: Rust requires explicit conversions between numeric types
- Precision loss: Converting from larger to smaller types can lose data
- Wrong signedness: Mixing signed and unsigned types requires careful handling
Checks for Understanding
- What's the default integer type in Rust?
- What's the difference between
char
andu8
? - How do you access the second element of a tuple?
- What happens when you try to assign an
i64
value to ani32
variable? - What's the difference between an array and a tuple?
Answers
i32
- 32-bit signed integerchar
is 4 bytes and represents Unicode;u8
is 1 byte and represents raw bytes- Use dot notation with index:
tuple.1
- Compile error - you must explicitly cast with
as i32
- Arrays hold multiple values of the same type; tuples hold multiple values of potentially different types