Data Types
Rust is a statically typed language, meaning the type of every variable must be known at compile time. Rust's type system is designed to prevent common programming errors while providing zero-cost abstractions and excellent performance.
Type Inference
Rust can often infer types from context, but you can also explicitly annotate types:
fn main() {
let x = 5; // Type inferred as i32
let y: i32 = 5; // Explicit type annotation
let z: f64 = 3.14; // Must specify for floating-point disambiguation
}
Scalar Types
Scalar types represent single values. Rust has four primary scalar types:
Integer Types
Rust provides several integer types with different sizes and signedness:
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
fn main() {
// Signed integers (can be negative)
let a: i8 = -128; // Range: -128 to 127
let b: i32 = -2_147_483_648; // Default integer type
let c: i64 = 9_223_372_036_854_775_807;
// Unsigned integers (only positive)
let d: u8 = 255; // Range: 0 to 255
let e: u32 = 4_294_967_295;
let f: u64 = 18_446_744_073_709_551_615;
// Architecture-dependent
let g: isize = 100; // Size depends on architecture (32 or 64 bit)
let h: usize = 200; // Commonly used for indexing
}
Integer Literals
You can write integer literals in multiple ways:
fn main() {
let decimal = 98_222;
let hex = 0xff;
let octal = 0o77;
let binary = 0b1111_0000;
let byte = b'A'; // u8 only
// Type suffixes
let x = 57u8;
let y = 100_i32;
let z = 0xff_u8;
}
Integer Overflow
Rust checks for integer overflow in debug mode:
fn main() {
let mut x: u8 = 255;
// x = x + 1; // This would panic in debug mode
// Use wrapping methods to handle overflow explicitly
x = x.wrapping_add(1); // Results in 0
println!("Wrapped: {}", x);
// Other overflow methods
let y: u8 = 250;
println!("Saturating add: {}", y.saturating_add(10)); // 255
println!("Checked add: {:?}", y.checked_add(10)); // Some(255) or None
println!("Overflowing add: {:?}", y.overflowing_add(10)); // (255, true)
}
Floating-Point Types
Rust has two floating-point types:
fn main() {
let x = 2.0; // f64 (default)
let y: f32 = 3.0; // f32
// Scientific notation
let large = 1e6; // 1,000,000.0
let small = 1e-6; // 0.000001
// Special values
let inf = f64::INFINITY;
let neg_inf = f64::NEG_INFINITY;
let nan = f64::NAN;
println!("Is NaN: {}", nan.is_nan());
println!("Is infinite: {}", inf.is_infinite());
}
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 = t && f; // false
let or = t || f; // true
let not = !t; // false
// Booleans are 1 byte in size
println!("Size of bool: {}", std::mem::size_of::<bool>());
}
Character Type
Rust's char
type represents a Unicode scalar value:
fn main() {
let c = 'z';
let z: char = 'ℤ'; // Unicode
let heart_eyed_cat = '😻';
// Characters are 4 bytes (Unicode scalar value)
println!("Size of char: {}", std::mem::size_of::<char>());
// Character methods
println!("Is alphabetic: {}", c.is_alphabetic());
println!("Is numeric: {}", '5'.is_numeric());
println!("To uppercase: {}", c.to_uppercase().collect::<String>());
}
Compound Types
Compound types group multiple values into one type.
Tuple Type
Tuples group together values of different types:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
// Destructuring
let (x, y, z) = tup;
println!("x: {}, y: {}, z: {}", x, y, z);
// Accessing by index
let five_hundred = tup.0;
let six_point_four = tup.1;
let one = tup.2;
// Empty tuple (unit type)
let unit: () = ();
// Single element tuple (note the comma)
let single = (42,);
// Nested tuples
let nested = ((1, 2), (3, 4));
println!("Nested access: {}", nested.0.1); // 2
}
Array Type
Arrays store multiple values of the same type with a fixed length:
fn main() {
// Array with explicit type and length
let a: [i32; 5] = [1, 2, 3, 4, 5];
// Array with repeated values
let b = [3; 5]; // [3, 3, 3, 3, 3]
// Accessing array elements
let first = a[0];
let second = a[1];
// Array length
println!("Array length: {}", a.len());
// Iterating over arrays
for element in a.iter() {
println!("Value: {}", element);
}
// Array slicing
let slice = &a[1..3]; // [2, 3]
println!("Slice: {:?}", slice);
}
String Types
Rust has several string-related types:
String Literals (&str
)
fn main() {
let s = "Hello, world!"; // &str (string slice)
let s: &str = "Hello, world!"; // Explicit type
// String slice methods
println!("Length: {}", s.len());
println!("Is empty: {}", s.is_empty());
println!("Contains 'world': {}", s.contains("world"));
}
Owned Strings (String
)
fn main() {
let mut s = String::new(); // Empty string
s.push_str("Hello");
s.push(' ');
s.push_str("world!");
// From string literal
let s2 = String::from("Hello, world!");
let s3 = "Hello, world!".to_string();
// String methods
println!("Capacity: {}", s.capacity());
println!("Length: {}", s.len());
// String concatenation
let s4 = s + &s2; // s is moved here
// println!("{}", s); // Error: s has been moved
}
Option and Result Types
These are fundamental enum types for handling nullable values and errors:
Option Type
fn main() {
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
// Pattern matching with Option
match some_number {
Some(value) => println!("Got a value: {}", value),
None => println!("Got nothing"),
}
// Option methods
let x = Some(5);
println!("Is some: {}", x.is_some());
println!("Is none: {}", x.is_none());
println!("Unwrap or default: {}", x.unwrap_or(0));
}
Result Type
fn main() {
let success: Result<i32, &str> = Ok(42);
let failure: Result<i32, &str> = Err("Something went wrong");
// Pattern matching with Result
match success {
Ok(value) => println!("Success: {}", value),
Err(error) => println!("Error: {}", error),
}
// Result methods
println!("Is ok: {}", success.is_ok());
println!("Is err: {}", failure.is_err());
println!("Unwrap or default: {}", failure.unwrap_or(0));
}
Type Aliases
You can create type aliases for complex or frequently used types:
type Kilometers = i32;
type Point = (i32, i32);
type Result<T> = std::result::Result<T, std::io::Error>;
fn main() {
let distance: Kilometers = 100;
let origin: Point = (0, 0);
let file_result: Result<String> = Ok("file contents".to_string());
}
Type Conversion
Rust doesn't perform implicit type conversions, but provides explicit conversion methods:
Casting with as
fn main() {
let x = 42i32;
let y = x as f64; // Explicit cast
let z = x as u8; // Truncation may occur
// Be careful with casting - data loss can occur
let large = 300i32;
let small = large as u8; // Results in 44 (300 - 256)
println!("Truncated: {}", small);
}
Using From
and Into
Traits
fn main() {
// From trait
let s = String::from("hello");
let num = i32::from(5u8);
// Into trait (usually inferred)
let s2: String = "hello".into();
let num2: i32 = 5u8.into();
// TryFrom for fallible conversions
use std::convert::TryFrom;
let result = i32::try_from(300u16); // Ok(300)
let error = i32::try_from(std::u16::MAX); // Still Ok for u16 to i32
match u8::try_from(300i32) {
Ok(value) => println!("Converted: {}", value),
Err(e) => println!("Conversion failed: {}", e),
}
}
Custom Types
You can define your own types using structs and enums:
Structs
struct Point {
x: i32,
y: i32,
}
struct Color(i32, i32, i32); // Tuple struct
struct UnitStruct; // Unit struct
fn main() {
let p = Point { x: 10, y: 20 };
let red = Color(255, 0, 0);
let unit = UnitStruct;
println!("Point: ({}, {})", p.x, p.y);
println!("Red component: {}", red.0);
}
Enums
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::Write(String::from("hello"));
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Text: {}", text),
Message::ChangeColor(r, g, b) => println!("Color: ({}, {}, {})", r, g, b),
}
}
Generic Types
Rust supports generic types for writing flexible, reusable code:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point { x, y }
}
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
println!("The largest number is {}", result);
let integer_point = Point::new(5, 10);
let float_point = Point::new(1.0, 4.0);
}
Memory Layout and Size
Understanding type sizes can be important for performance:
fn main() {
println!("Size of i8: {}", std::mem::size_of::<i8>()); // 1
println!("Size of i32: {}", std::mem::size_of::<i32>()); // 4
println!("Size of i64: {}", std::mem::size_of::<i64>()); // 8
println!("Size of f64: {}", std::mem::size_of::<f64>()); // 8
println!("Size of bool: {}", std::mem::size_of::<bool>()); // 1
println!("Size of char: {}", std::mem::size_of::<char>()); // 4
// Compound types
println!("Size of (i32, i32): {}", std::mem::size_of::<(i32, i32)>()); // 8
println!("Size of [i32; 5]: {}", std::mem::size_of::<[i32; 5]>()); // 20
// Reference types
println!("Size of &i32: {}", std::mem::size_of::<&i32>()); // 8 on 64-bit
println!("Size of &str: {}", std::mem::size_of::<&str>()); // 16 (ptr + len)
println!("Size of String: {}", std::mem::size_of::<String>()); // 24 (ptr + len + cap)
}
Understanding Rust's type system is fundamental to writing safe and efficient Rust code. The type system prevents many common programming errors at compile time while providing excellent performance through zero-cost abstractions.