Primitive Types & Enums
Rust's type system is expressive, precise, and designed to catch bugs at compile time. Primitives are explicit about their sizes. Enums are more powerful than in any other mainstream language. Option<T> replaces null. Result<T, E> replaces exceptions. Once you understand these types, you understand how Rust models data.
Integer Types
Rust integers have explicit sizes. No guessing whether int is 32 or 64 bits.
fn main() {
let a: i8 = -128; // signed 8-bit: -128 to 127
let b: u8 = 255; // unsigned 8-bit: 0 to 255
let c: i32 = -2_000_000; // signed 32-bit (default integer type)
let d: u64 = 18_000_000_000_000;
let e: isize = -1; // pointer-sized signed (64-bit on 64-bit systems)
let f: usize = 42; // pointer-sized unsigned (used for indexing)
println!("{} {} {} {} {} {}", a, b, c, d, e, f);
}
-128 255 -2000000 18000000000000 -1 42
i32 is the default when you write let x = 42. Use usize for collection indices and sizes. Use specific widths when you care about memory layout or protocol compatibility.
Underscores in numeric literals (2_000_000) are visual separators. Use them freely for readability.
Floating Point, Bool & Char
fn main() {
let pi: f64 = 3.14159265358979; // 64-bit float (default)
let approx: f32 = 3.14; // 32-bit float
let active: bool = true;
let emoji: char = '🦀'; // char is 4 bytes (Unicode scalar value)
println!("{:.4} {} {} {}", pi, approx, active, emoji);
}
3.1416 3.14 true 🦀
f64 is the default float type. char is not a byte — it is a Unicode scalar value and always 4 bytes wide.
str vs String
This trips up every Rust beginner. There are two string types and you need both:
fn main() {
// &str: a borrowed string slice. Fixed content, no allocation.
let greeting: &str = "hello"; // string literal, stored in the binary
// String: an owned, heap-allocated, growable string.
let mut name = String::from("world");
name.push('!');
// Converting between them:
let owned: String = greeting.to_string(); // &str -> String
let borrowed: &str = &name; // String -> &str (auto-deref)
println!("{}, {}", greeting, name);
println!("{}, {}", owned, borrowed);
}
hello, world!
hello, world!
Rule of thumb: use &str for function parameters (accepts both types). Use String when you need to own or modify the string.
Enums: More Powerful Than You Think
Rust enums are algebraic data types. Each variant can hold different data. This is not a list of integer constants — it is a way to model distinct states with associated data.
enum IpAddress {
V4(u8, u8, u8, u8),
V6(String),
}
enum Command {
Quit,
Echo(String),
Move { x: i32, y: i32 },
Color(u8, u8, u8),
}
fn handle_command(cmd: Command) {
match cmd {
Command::Quit => println!("Quitting"),
Command::Echo(msg) => println!("Echo: {}", msg),
Command::Move { x, y } => println!("Moving to ({}, {})", x, y),
Command::Color(r, g, b) => println!("Color: #{:02x}{:02x}{:02x}", r, g, b),
}
}
fn main() {
let addr = IpAddress::V4(127, 0, 0, 1);
match addr {
IpAddress::V4(a, b, c, d) => println!("{}.{}.{}.{}", a, b, c, d),
IpAddress::V6(s) => println!("{}", s),
}
handle_command(Command::Move { x: 10, y: 20 });
handle_command(Command::Color(255, 128, 0));
}
127.0.0.1
Moving to (10, 20)
Color: #ff8000
Each variant is a distinct type that can carry its own data. match destructures them exhaustively — the compiler ensures you handle every case.
Option: No More Null
Rust has no null. Instead, the standard library provides Option<T>:
fn find_user(id: u64) -> Option<String> {
match id {
1 => Some(String::from("Alice")),
2 => Some(String::from("Bob")),
_ => None,
}
}
fn main() {
// You must handle both cases. The compiler enforces this.
let user = find_user(1);
match user {
Some(name) => println!("Found: {}", name),
None => println!("Not found"),
}
// Shorter alternatives:
if let Some(name) = find_user(2) {
println!("Found: {}", name);
}
// unwrap_or provides a default:
let name = find_user(99).unwrap_or(String::from("anonymous"));
println!("User: {}", name);
// map transforms the inner value:
let upper = find_user(1).map(|n| n.to_uppercase());
println!("{:?}", upper);
}
Found: Alice
Found: Bob
User: anonymous
Some("ALICE")
Every Option must be explicitly handled before you can access the value. This eliminates null pointer exceptions entirely.
Result<T, E>: No More Exceptions
Rust has no exceptions. Operations that can fail return Result<T, E>:
use std::fs;
use std::num::ParseIntError;
fn parse_port(s: &str) -> Result<u16, ParseIntError> {
s.parse::<u16>()
}
fn main() {
// Handling Result explicitly:
match parse_port("8080") {
Ok(port) => println!("Port: {}", port),
Err(e) => println!("Invalid port: {}", e),
}
match parse_port("not_a_number") {
Ok(port) => println!("Port: {}", port),
Err(e) => println!("Invalid port: {}", e),
}
// Reading a file returns Result:
match fs::read_to_string("/etc/hostname") {
Ok(contents) => println!("Hostname: {}", contents.trim()),
Err(e) => println!("Could not read hostname: {}", e),
}
}
Port: 8080
Invalid port: invalid digit found in string
Could not read hostname: No such file or directory (os error 2)
Result makes error handling visible in the type signature. You cannot ignore errors — the compiler forces you to deal with them.
Pattern Matching with match
match is exhaustive, type-safe, and the primary way to work with enums:
enum TrafficLight {
Red,
Yellow,
Green,
}
fn action(light: &TrafficLight) -> &str {
match light {
TrafficLight::Red => "stop",
TrafficLight::Yellow => "caution",
TrafficLight::Green => "go",
}
}
fn describe_number(n: i32) -> String {
match n {
0 => String::from("zero"),
1..=9 => String::from("single digit"),
10 | 20 | 30 => String::from("round tens"),
n if n < 0 => format!("negative: {}", n),
_ => format!("other: {}", n),
}
}
fn main() {
let light = TrafficLight::Green;
println!("{}", action(&light));
for n in [-5, 0, 3, 10, 42] {
println!("{}: {}", n, describe_number(n));
}
}
go
-5: negative: -5
0: zero
3: single digit
10: round tens
42: other: 42
If you forget a variant, the compiler tells you. If you add a new variant to an enum, every match that handles it must be updated. This is a feature — it prevents the "I added a case but forgot to handle it" class of bugs.
Combining Option & Result
Real-world code chains these types together:
fn parse_header_value(headers: &str, key: &str) -> Option<u64> {
headers
.lines()
.find(|line| line.starts_with(key)) // Option<&str>
.and_then(|line| line.split(':').nth(1)) // Option<&str>
.and_then(|val| val.trim().parse().ok()) // Option<u64>
}
fn main() {
let headers = "Content-Type: application/json\nContent-Length: 1024\nHost: example.com";
match parse_header_value(headers, "Content-Length") {
Some(len) => println!("Body size: {} bytes", len),
None => println!("No content length"),
}
match parse_header_value(headers, "X-Missing") {
Some(val) => println!("Found: {}", val),
None => println!("Header not found"),
}
}
Body size: 1024 bytes
Header not found
Common Pitfalls
- Using
unwrap()onOptionorResultin production code. It panics onNone/Err. Usematch,if let,unwrap_or, or?instead. - Confusing
&strandString. Think of&stras a view into string data andStringas an owned buffer. Functions should usually accept&str. - Not using enums when you should. If a value can be one of several distinct states, use an enum. Do not reach for stringly-typed code or boolean flags.
- Forgetting that integer overflow panics in debug mode. In release mode, it wraps. If you need specific behavior, use
wrapping_add,checked_add, orsaturating_add. - Treating
Option<T>like a nullable pointer. It is a proper enum. Use the combinators (map,and_then,unwrap_or_else) instead of nestingif letstatements. - Using
asfor numeric casts without checking.x as u8truncates silently. Useu8::try_from(x)for safe conversion.
Key Takeaways
- Rust integers have explicit sizes.
i32is the default.usizeis for indexing. &stris a borrowed string slice;Stringis an owned, growable string. Know when to use each.- Enums in Rust carry data per variant. They are algebraic data types, not integer constants.
Option<T>replaces null.Result<T, E>replaces exceptions. Both must be handled explicitly.matchis exhaustive — the compiler ensures every case is covered.- Favor combinators (
map,and_then,unwrap_or) over nested pattern matching for cleaner code.