Data Types in Rust
Every value in Rust has a type, and the compiler must know that type at compile time. Rust is a statically-typed language. Most of the time the compiler infers the type for you; occasionally you must provide an explicit annotation. Understanding Rust's type system is fundamental to writing correct, efficient code.
Rust's types fall into two broad categories: scalar types (a single value) and compound types (multiple values grouped together).
Scalar Types
A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, booleans, and characters.
Integer Types
An integer is a whole number — no decimal point. Rust provides signed integers (which can be negative) and unsigned integers (always non-negative). The number in the type name is the bit width.
Type | Signed? | Size | Min value | Max value |
|---|---|---|---|---|
i8 | Yes | 8-bit | -128 | 127 |
i16 | Yes | 16-bit | -32,768 | 32,767 |
i32 | Yes | 32-bit | -2,147,483,648 | 2,147,483,647 |
i64 | Yes | 64-bit | -9.2 × 10¹⁸ | 9.2 × 10¹⁸ |
i128 | Yes | 128-bit | -1.7 × 10³⁸ | 1.7 × 10³⁸ |
isize | Yes | pointer-sized | platform-dependent | platform-dependent |
u8 | No | 8-bit | 0 | 255 |
u16 | No | 16-bit | 0 | 65,535 |
u32 | No | 32-bit | 0 | 4,294,967,295 |
u64 | No | 64-bit | 0 | 1.8 × 10¹⁹ |
u128 | No | 128-bit | 0 | 3.4 × 10³⁸ |
usize | No | pointer-sized | 0 | platform-dependent |
i32 — generally the fastest even on 64-bit systems. Use usize and isize for indexing collections and pointer arithmetic.fn main() {
let a: i8 = -100;
let b: u8 = 200;
let c: i32 = -2_147_483_648; // i32 minimum
let d: u32 = 4_294_967_295; // u32 maximum
let e: i64 = 9_223_372_036_854_775_807;
let f: usize = 42; // typically used for indexing
println!("i8={} u8={} i32={} u32={} i64={} usize={}", a, b, c, d, e, f);
}i8=-100 u8=200 i32=-2147483648 u32=4294967295 i64=9223372036854775807 usize=42
Integer Overflow
Integer overflow occurs when a calculation produces a value outside the type's range. Rust handles this differently depending on the build profile.
--release), overflow wraps around using two's complement arithmetic (e.g. 255u8 + 1 becomes 0). Never rely on wrapping behaviour in release mode unless you use the explicit wrapping methods.fn main() {
let max: u8 = 255;
// This panics in debug mode:
// let overflow = max + 1;
// Use explicit methods to control overflow behaviour:
let wrapped = max.wrapping_add(1); // 0 (wraps around)
let checked = max.checked_add(1); // None (returns Option)
let saturated = max.saturating_add(1); // 255 (clamps to max)
let (val, overflowed) = max.overflowing_add(1); // (0, true)
println!("wrapping: {}", wrapped);
println!("checked: {:?}", checked);
println!("saturated: {}", saturated);
println!("overflowing: val={} overflowed={}", val, overflowed);
}wrapping: 0 checked: None saturated: 255 overflowing: val=0 overflowed=true
Floating-Point Types
Rust has two floating-point types: f32 (32-bit, single precision) and f64
(64-bit, double precision). The default is f64 — it is the same speed as f32
on modern hardware and offers more precision.
fn main() {
let x = 2.0; // f64 by default
let y: f32 = 3.0; // explicit f32
let sum = x + 2.5;
let diff = x - 1.1;
let prod = x * y as f64;
let quot = x / 0.7;
let rem = x % 1.5;
println!("sum={:.2} diff={:.2} prod={:.2} quot={:.2} rem={:.2}",
sum, diff, prod, quot, rem);
}sum=4.50 diff=0.90 prod=6.00 quot=2.86 rem=0.50
Boolean Type
The bool type has exactly two values: true and false. Booleans are one byte
in size and are the result of comparison and logical operations.
fn main() {
let t = true;
let f: bool = false; // explicit annotation
// Logical operators
println!("AND: {}", t && f); // false
println!("OR: {}", t || f); // true
println!("NOT: {}", !t); // false
// Comparisons produce bools
let x = 5;
let is_even = x % 2 == 0;
let is_big = x > 100;
println!("is_even={} is_big={}", is_even, is_big);
}AND: false OR: true NOT: false is_even=false is_big=false
The char Type
Rust's char type represents a single Unicode Scalar Value. It is specified with
single quotes (strings use double quotes). Crucially, char is four bytes in
size — large enough to hold any Unicode character, including emoji and characters
from non-Latin scripts.
fn main() {
let letter: char = 'A';
let heart: char = '❤'; // ❤ via Unicode escape
let emoji: char = '🦀'; // Rust's mascot, Ferris
println!("{} {} {}", letter, heart, emoji);
println!("Size of char: {} bytes", std::mem::size_of::<char>());
println!("Is alphabetic: {}", letter.is_alphabetic());
println!("Is numeric: {}", '9'.is_numeric());
println!("To uppercase: {}", 'a'.to_uppercase().next().unwrap());
}A ❤ 🦀 Size of char: 4 bytes Is alphabetic: true Is numeric: true To uppercase: A
char (4 bytes, Unicode Scalar Value) with a single byte. String slices (&str) are UTF-8 encoded, so indexing by byte offset is not the same as indexing by character.Compound Types — Tuples
A tuple groups together values of different types into a single compound value. Tuples have a fixed length — once declared, they cannot grow or shrink. Access individual elements with dot notation and a zero-based index.
fn main() {
let person: (&str, u8, f64) = ("Alice", 28, 1.72);
// Access by index
let name = person.0;
let age = person.1;
let height = person.2;
println!("{} is {} years old, {:.2}m tall", name, age, height);
// Destructure into separate bindings
let (city, population, area) = ("Berlin", 3_645_000_u32, 891.8_f64);
println!("{}: {} people, {:.1} km²", city, population, area);
// Unit tuple — the empty tuple () — is Rust's "nothing" type
let unit: () = ();
println!("Unit value: {:?}", unit);
}Alice is 28 years old, 1.72m tall Berlin: 3645000 people, 891.8 km² Unit value: ()
Compound Types — Arrays
An array groups together values of the same type into a fixed-length sequence stored contiguously on the stack. Unlike vectors, arrays cannot grow or shrink at runtime. Arrays are ideal when you know the exact number of elements at compile time.
fn main() {
// Declaration: [type; length]
let primes: [u32; 5] = [2, 3, 5, 7, 11];
// Access by index (zero-based)
println!("First prime: {}", primes[0]);
println!("Last prime: {}", primes[4]);
// Array length
println!("Count: {}", primes.len());
// Initialise all elements to the same value: [value; length]
let zeros = [0u8; 8];
println!("zeros: {:?}", zeros);
// Iterating
for prime in &primes {
print!("{} ", prime);
}
println!();
}First prime: 2 Last prime: 11 Count: 5 zeros: [0, 0, 0, 0, 0, 0, 0, 0] 2 3 5 7 11
primes[10]) causes a runtime panic in Rust, not undefined behaviour. Rust checks bounds on every array access unless the compiler can prove at compile time that the index is valid.Type Inference vs Explicit Annotations
Rust's type inference is powerful enough to handle most everyday code. You only need explicit annotations when:
The compiler says it cannot infer the type (e.g. parsing a string into a number)
You want a type different from the default (e.g.
u8instead of the defaulti32)Clarity for the reader outweighs brevity (documenting the expected range)
You are writing a public API where the types should be obvious from the signature
fn main() {
// Inferred — works fine
let x = 42;
let y = 3.14;
let flag = true;
// Must be explicit — the compiler cannot guess which integer type to parse into
let parsed: i32 = "100".parse().unwrap();
// Want u8, not the default i32
let byte: u8 = 255;
println!("x={} y={} flag={} parsed={} byte={}", x, y, flag, parsed, byte);
}x=42 y=3.14 flag=true parsed=100 byte=255
Type Casting with as
The as keyword performs explicit type conversion between numeric types. It is not
the same as a safe conversion — it can truncate or reinterpret bits, so use it
carefully.
fn main() {
let x: i32 = 300;
let y = x as u8; // 300 truncates to 44 (300 - 256)
println!("300 as u8 = {}", y);
let big: i32 = -1;
let unsigned = big as u32; // reinterprets as 4294967295
println!("-1 as u32 = {}", unsigned);
let f = 3.99_f64;
let i = f as i32; // truncates towards zero, not rounds
println!("3.99 as i32 = {}", i); // 3
let neg = -3.99_f64;
let neg_i = neg as i32;
println!("-3.99 as i32 = {}", neg_i); // -3
}300 as u8 = 44 -1 as u32 = 4294967295 3.99 as i32 = 3 -3.99 as i32 = -3
as truncates floats towards zero — it does not round. Use.round() as i32 if you need rounding behaviour. It also silently truncates integers when casting to a narrower type.usize and isize
usize and isize are pointer-sized integer types — 32 bits on a 32-bit platform
and 64 bits on a 64-bit platform. They are used wherever the size or index of a
memory region is needed.
usize — use for array indices, slice lengths, and loop counters over collections
isize — use for pointer arithmetic or when a difference between two pointers could be negative
Rust requires array and slice indices to be
usize— the compiler will tell you if you pass ani32On most modern hardware both are 64 bits, but portable code should not assume this
fn main() {
let items = [10, 20, 30, 40, 50];
let index: usize = 2; // must be usize for indexing
println!("items[{}] = {}", index, items[index]);
let len: usize = items.len(); // len() returns usize
println!("length: {}", len);
// Pointer size
println!("usize is {} bytes on this platform",
std::mem::size_of::<usize>());
}items[2] = 30 length: 5 usize is 8 bytes on this platform