Introduction to Rust Data Types
Rust is a statically typed language, meaning it must know the types of all variables at compile time. The compiler can usually infer the type based on the value and how it's used, but sometimes you need to annotate types explicitly. Rust's type system is designed to be expressive while maintaining memory safety and performance.
Key Concepts
- Static Typing: All types are known at compile time
- Type Inference: Compiler can deduce types in most cases
- Strong Typing: Limited implicit conversions between types
- Zero-Cost Abstractions: Types don't add runtime overhead
- Memory Safety: Type system prevents many common bugs
1. Scalar Types
Scalar types represent a single value. Rust has four primary scalar types: integers, floating-point numbers, booleans, and characters.
Integer Types
fn main() { // Signed integers (can be negative) let a: i8 = -128; // -128 to 127 let b: i16 = -32768; // -32768 to 32767 let c: i32 = -2147483648; // -2^31 to 2^31-1 (default) let d: i64 = -9223372036854775808; let e: i128 = -170141183460469231731687303715884105728; let f: isize = -100; // Architecture-dependent (32 or 64 bits) // Unsigned integers (non-negative) let g: u8 = 255; // 0 to 255 let h: u16 = 65535; // 0 to 65535 let i: u32 = 4294967295; // 0 to 2^32-1 let j: u64 = 18446744073709551615; let k: u128 = 340282366920938463463374607431768211455; let l: usize = 100; // Architecture-dependent // Integer literals let decimal = 98_222; // Decimal with underscores for readability let hex = 0xff; // Hexadecimal let octal = 0o77; // Octal let binary = 0b1111_0000; // Binary let byte = b'A'; // Byte (u8 only) println!("Decimal: {}", decimal); println!("Hex: {}", hex); println!("Octal: {}", octal); println!("Binary: {:b}", binary); println!("Byte: {}", byte); // Integer operations let sum = 5 + 10; let difference = 95.5 - 4.3; let product = 4 * 30; let quotient = 56.7 / 32.2; let remainder = 43 % 5; // Integer overflow behavior (debug vs release) let mut x: u8 = 255; // In debug builds, this would panic // In release builds, it wraps to 0 // x = x + 1; // Uncomment to see behavior // Safe alternatives to handle overflow let y = 255u8; println!("Checked add: {:?}", y.checked_add(1)); // None println!("Saturating add: {}", y.saturating_add(1)); // 255 println!("Wrapping add: {}", y.wrapping_add(1)); // 0 println!("Overflowing add: {:?}", y.overflowing_add(1)); // (0, true) } Floating-Point Types
fn main() { // f32 (32-bit float, single precision) let f32_val: f32 = 3.14; // f64 (64-bit float, double precision) - default let f64_val = 3.141592653589793; // Scientific notation let scientific = 2.5e5; // 250000 let scientific_neg = 1.5e-3; // 0.0015 println!("f32: {}", f32_val); println!("f64: {}", f64_val); println!("Scientific: {}", scientific); // Floating-point operations let sum = 5.0 + 10.2; let difference = 95.5 - 4.3; let product = 4.0 * 30.0; let quotient = 56.7 / 32.2; // Special floating-point values let infinity = f64::INFINITY; let neg_infinity = f64::NEG_INFINITY; let nan = f64::NAN; let min = f64::MIN; let max = f64::MAX; let min_positive = f64::MIN_POSITIVE; println!("Infinity: {}", infinity); println!("NaN: {}", nan); println!("NaN check: {}", nan.is_nan()); // Floating-point precision issues let result = 0.1 + 0.2; println!("0.1 + 0.2 = {}", result); // 0.30000000000000004 // Comparing floats (use epsilon) let a = 0.1 + 0.2; let b = 0.3; let epsilon = f64::EPSILON; if (a - b).abs() < epsilon { println!("Approximately equal"); } // Rounding let num = 3.14159; println!("Round: {}", num.round()); // 3 println!("Floor: {}", num.floor()); // 3 println!("Ceil: {}", num.ceil()); // 4 println!("Trunc: {}", num.trunc()); // 3 println!("Fract: {}", num.fract()); // 0.14159 } Boolean Type
fn main() { // Boolean values let t = true; let f: bool = false; // Boolean operations let and = t && f; // Logical AND let or = t || f; // Logical OR let not = !t; // Logical NOT println!("AND: {}", and); println!("OR: {}", or); println!("NOT: {}", not); // Boolean from comparisons let greater = 5 > 3; let less = 5 < 3; let equal = 5 == 5; let not_equal = 5 != 3; // Boolean in conditions if greater { println!("5 is greater than 3"); } // Boolean as integer (cast) let bool_as_int = t as i32; // 1 let bool_as_int2 = f as i32; // 0 println!("true as i32: {}", bool_as_int); // Boolean methods let flag = true; println!("then: {}", flag.then(|| 42).unwrap()); // 42 println!("then_some: {}", flag.then_some("yes").unwrap()); // "yes" } Character Type
fn main() { // Characters are 4 bytes (Unicode scalar values) let c = 'z'; let z = 'ℤ'; let heart_eyed_cat = '😻'; println!("c: {}", c); println!("z: {}", z); println!("cat: {}", heart_eyed_cat); // Character literals with escapes let tab = '\t'; let newline = '\n'; let carriage_return = '\r'; let single_quote = '\''; let double_quote = '"'; let backslash = '\\'; // Unicode escapes let omega = '\u{03A9}'; // Ω let smiley = '\u{1F600}'; // 😀 println!("Omega: {}", omega); println!("Smiley: {}", smiley); // Character operations let ch = 'a'; println!("Is alphabetic: {}", ch.is_alphabetic()); println!("Is numeric: {}", ch.is_numeric()); println!("Is whitespace: {}", ch.is_whitespace()); println!("To uppercase: {}", ch.to_uppercase().collect::<String>()); // Character conversions let digit = '5'; let digit_num = digit.to_digit(10).unwrap(); println!("Digit as number: {}", digit_num); // Character from u8 let byte_char = 65 as char; println!("65 as char: {}", byte_char); // 'A' // Character size println!("Size of char: {} bytes", std::mem::size_of::<char>()); // 4 bytes } 2. Compound Types
Compound types group multiple values into one type.
Tuples
fn main() { // Tuple declaration let tup: (i32, f64, char) = (500, 6.4, 'q'); // Type inference let tup2 = (500, 6.4, 'q'); // Accessing tuple elements let (x, y, z) = tup; // Destructuring println!("x: {}, y: {}, z: {}", x, y, z); // Index access let first = tup.0; let second = tup.1; let third = tup.2; println!("first: {}, second: {}, third: {}", first, second, third); // Single-element tuple (note the comma) let single = (5,); // Tuple with one element let not_tuple = (5); // Just a number in parentheses // Nested tuples let nested = (1, (2, 3), 4); println!("nested.1.1: {}", (nested.1).1); // 3 // Unit type - empty tuple let unit = (); println!("Unit type: {:?}", unit); // Functions return unit implicitly fn returns_unit() {} // Tuples in match expressions let pair = (1, 2); match pair { (0, y) => println!("First is 0, second is {}", y), (x, 0) => println!("Second is 0, first is {}", x), (x, y) => println!("Both non-zero: {}, {}", x, y), } // Tuple as function argument fn sum_tuple(t: (i32, i32)) -> i32 { t.0 + t.1 } let result = sum_tuple((5, 10)); println!("Sum: {}", result); // Tuple size println!("Size of tuple (i32, f64): {} bytes", std::mem::size_of::<(i32, f64)>()); } Arrays
fn main() { // Fixed-size arrays (stack allocated) let arr: [i32; 5] = [1, 2, 3, 4, 5]; // Type inference let arr2 = [1, 2, 3, 4, 5]; // Array with same value for all elements let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] let threes = [3; 5]; // [3, 3, 3, 3, 3] // Accessing array elements let first = arr[0]; let second = arr[1]; println!("First: {}, Second: {}", first, second); // Array length let length = arr.len(); println!("Array length: {}", length); // Array slicing let slice = &arr[1..4]; // [2, 3, 4] let full_slice = &arr[..]; // entire array let from_start = &arr[2..]; // [3, 4, 5] let to_end = &arr[..3]; // [1, 2, 3] println!("Slice: {:?}", slice); // Array iteration for element in arr.iter() { println!("Element: {}", element); } // Mutable array let mut mut_arr = [1, 2, 3, 4, 5]; mut_arr[0] = 10; println!("Modified array: {:?}", mut_arr); // Array bounds checking // let out_of_bounds = arr[10]; // This would panic at runtime // Safe access with get() match arr.get(10) { Some(value) => println!("Value: {}", value), None => println!("Index out of bounds"), } // Multidimensional arrays let matrix: [[i32; 3]; 2] = [ [1, 2, 3], [4, 5, 6], ]; println!("matrix[0][1]: {}", matrix[0][1]); // 2 // Array operations let arr1 = [1, 2, 3]; let arr2 = [4, 5, 6]; // Arrays implement useful traits println!("Is empty: {}", arr1.is_empty()); println!("First: {:?}", arr1.first()); println!("Last: {:?}", arr1.last()); // Array size at compile time const ARRAY_SIZE: usize = 5; let fixed: [i32; ARRAY_SIZE] = [0; ARRAY_SIZE]; println!("Fixed size array: {:?}", fixed); } 3. String Types
String vs &str
fn main() { // String (owned, heap-allocated, mutable) let mut s = String::new(); s.push_str("hello"); s.push(' '); s.push_str("world"); println!("String: {}", s); // String from literal let s2 = String::from("hello world"); let s3 = "hello world".to_string(); // &str (string slice, borrowed, immutable) let slice: &str = "hello world"; // String literal let slice2 = &s2[..]; // Slice from String // Differences and conversions let string = String::from("hello"); let str_slice: &str = &string; // &String -> &str let new_string = str_slice.to_string(); // &str -> String let new_string2 = String::from(str_slice); // String operations let mut s = String::from("foo"); s.push_str("bar"); // Append string slice s.push('!'); // Append character // Concatenation let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // s1 is moved here println!("s3: {}", s3); // Better concatenation with format! let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{}-{}-{}", s1, s2, s3); println!("s: {}", s); // String indexing (not allowed) // let ch = s[0]; // ERROR: cannot index into String // String slicing (careful with UTF-8) let hello = "Здравствуйте"; let slice = &hello[0..4]; // "Зд" (each char is 2 bytes) println!("Slice: {}", slice); // Iterating over strings for c in "नमस्ते".chars() { println!("Char: {}", c); } for b in "hello".bytes() { println!("Byte: {}", b); } } String Methods
fn main() { let mut s = String::from(" Hello, World! "); // Trimming println!("Trim: '{}'", s.trim()); println!("Trim start: '{}'", s.trim_start()); println!("Trim end: '{}'", s.trim_end()); // Case conversion println!("Lowercase: {}", s.to_lowercase()); println!("Uppercase: {}", s.to_uppercase()); // Checking content println!("Contains 'Hello': {}", s.contains("Hello")); println!("Starts with ' ': {}", s.starts_with(" ")); println!("Ends with '! ': {}", s.ends_with("! ")); // Finding println!("Find 'World': {:?}", s.find("World")); println!("Find 'xyz': {:?}", s.find("xyz")); // Replacing println!("Replace: {}", s.replace("World", "Rust")); println!("Replace pattern: {}", s.replace(char::is_whitespace, "_")); // Splitting let parts: Vec<&str> = s.split(',').collect(); println!("Split: {:?}", parts); let words: Vec<&str> = s.split_whitespace().collect(); println!("Words: {:?}", words); // Lines let multiline = "line1\nline2\nline3"; for line in multiline.lines() { println!("Line: {}", line); } // Substrings let sub = &s[2..7]; // "Hello" println!("Substring: {}", sub); // Length and capacity println!("Length: {}", s.len()); println!("Capacity: {}", s.capacity()); println!("Is empty: {}", s.is_empty()); // Modifying in-place s.clear(); println!("After clear: '{}'", s); // Building strings efficiently let mut builder = String::with_capacity(100); for i in 0..10 { builder.push_str(&format!("item-{} ", i)); } println!("Builder: {}", builder); } 4. Vector Types
Vec
fn main() { // Creating vectors let mut v: Vec<i32> = Vec::new(); let v2 = vec![1, 2, 3, 4, 5]; let v3 = vec![0; 10]; // Ten zeros // Adding elements v.push(5); v.push(6); v.push(7); println!("v: {:?}", v); // Accessing elements let third: &i32 = &v[2]; println!("Third element: {}", third); match v.get(2) { Some(third) => println!("Third element: {}", third), None => println!("No third element"), } // Safe access match v.get(10) { Some(value) => println!("Value: {}", value), None => println!("Index out of bounds"), } // Iterating for i in &v { println!("Element: {}", i); } // Mutable iteration for i in &mut v { *i *= 2; } println!("Doubled: {:?}", v); // Removing elements let last = v.pop(); // Removes last element println!("Popped: {:?}, v: {:?}", last, v); let first = v.remove(0); // Removes element at index println!("Removed first: {}, v: {:?}", first, v); // Inserting v.insert(0, 10); v.insert(2, 20); println!("After insert: {:?}", v); // Vector operations println!("Length: {}", v.len()); println!("Capacity: {}", v.capacity()); println!("Is empty: {}", v.is_empty()); // Resizing v.resize(5, 0); println!("After resize: {:?}", v); // Sorting let mut unsorted = vec![3, 1, 4, 1, 5, 9, 2, 6]; unsorted.sort(); println!("Sorted: {:?}", unsorted); // Reverse unsorted.reverse(); println!("Reversed: {:?}", unsorted); // Filtering and transforming let numbers = vec![1, 2, 3, 4, 5, 6]; let evens: Vec<_> = numbers.iter() .filter(|&&x| x % 2 == 0) .collect(); println!("Evens: {:?}", evens); let squares: Vec<_> = numbers.iter() .map(|&x| x * x) .collect(); println!("Squares: {:?}", squares); // Vector of different types using enum #[derive(Debug)] enum MixedType { Int(i32), Float(f64), Text(String), } let mixed = vec![ MixedType::Int(42), MixedType::Float(3.14), MixedType::Text(String::from("hello")), ]; println!("Mixed: {:?}", mixed); } 5. Struct Types
Basic Structs
// Named field struct struct User { username: String, email: String, sign_in_count: u64, active: bool, } // Tuple struct (named tuple) struct Color(i32, i32, i32); struct Point(i32, i32, i32); // Unit-like struct (no fields) struct AlwaysEqual; fn main() { // Creating instances let user1 = User { email: String::from("[email protected]"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; println!("User email: {}", user1.email); // Mutable instance let mut user2 = User { email: String::from("[email protected]"), username: String::from("anotheruser"), active: true, sign_in_count: 1, }; user2.email = String::from("[email protected]"); // Struct update syntax let user3 = User { email: String::from("[email protected]"), username: String::from("thirduser"), ..user1 // Copy remaining fields from user1 }; // Tuple structs let black = Color(0, 0, 0); let origin = Point(0, 0, 0); println!("Black RGB: ({}, {}, {})", black.0, black.1, black.2); // Unit-like struct let subject = AlwaysEqual; // Destructuring let Point(x, y, z) = origin; println!("Point: ({}, {}, {})", x, y, z); } // Factory function fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, } } Struct Methods
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } // Implementation block impl Rectangle { // Method (takes self) fn area(&self) -> u32 { self.width * self.height } // Method with parameters fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } // Associated function (doesn't take self) fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size, } } // Getter fn width(&self) -> u32 { self.width } // Setter (with validation) fn set_width(&mut self, width: u32) { if width > 0 { self.width = width; } } // Method that takes ownership fn destroy(self) { println!("Destroying rectangle: {:?}", self); // self dropped here } } // Multiple impl blocks impl Rectangle { fn is_square(&self) -> bool { self.width == self.height } fn diagonal(&self) -> f64 { let w = self.width as f64; let h = self.height as f64; (w * w + h * h).sqrt() } } fn main() { let rect = Rectangle { width: 30, height: 50, }; println!("Area: {}", rect.area()); println!("Width: {}", rect.width()); println!("Is square: {}", rect.is_square()); println!("Diagonal: {:.2}", rect.diagonal()); let square = Rectangle::square(20); println!("Square area: {}", square.area()); println!("Square holds rect: {}", square.can_hold(&rect)); let mut rect2 = Rectangle { width: 10, height: 10 }; rect2.set_width(15); println!("Updated width: {}", rect2.width()); // rect2.destroy(); // Uncomment to move ownership // println!("{:?}", rect2); // Error: rect2 moved } 6. Enum Types
Basic Enums
// Simple enum enum IpAddrKind { V4, V6, } // Enum with data enum IpAddr { V4(u8, u8, u8, u8), V6(String), } // Enum with different variants enum Message { Quit, // No data Move { x: i32, y: i32 }, // Anonymous struct Write(String), // Single string ChangeColor(i32, i32, i32), // Tuple } impl Message { fn call(&self) { match self { Message::Quit => println!("Quit"), Message::Move { x, y } => println!("Move to ({}, {})", x, y), Message::Write(text) => println!("Write: {}", text), Message::ChangeColor(r, g, b) => { println!("Change color to RGB({}, {}, {})", r, g, b); } } } } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); let m1 = Message::Quit; let m2 = Message::Move { x: 10, y: 20 }; let m3 = Message::Write(String::from("hello")); let m4 = Message::ChangeColor(255, 0, 0); m1.call(); m2.call(); m3.call(); m4.call(); } Option Enum
fn main() { // Option<T> is built into Rust prelude let some_number = Some(5); let some_string = Some("a string"); let absent_number: Option<i32> = None; // Option vs null-safe operations let x: i8 = 5; let y: Option<i8> = Some(5); // Need to handle Option before using value match y { Some(value) => println!("Sum: {}", x + value), None => println!("No value"), } // if let syntax if let Some(value) = y { println!("Value: {}", value); } // Option combinators let maybe_value: Option<i32> = Some(10); let doubled = maybe_value.map(|x| x * 2); println!("Doubled: {:?}", doubled); let filtered = maybe_value.filter(|&x| x > 5); println!("Filtered: {:?}", filtered); let or_else = maybe_value.or(Some(20)); println!("Or else: {:?}", or_else); let unwrapped = maybe_value.unwrap_or(0); println!("Unwrapped or default: {}", unwrapped); // Chaining options let result = Some(5) .map(|x| x * 2) .filter(|&x| x > 5) .and_then(|x| if x < 20 { Some(x) } else { None }); println!("Chained result: {:?}", result); } Result Enum
use std::num::ParseIntError; fn main() { // Result<T, E> for error handling let success: Result<i32, &str> = Ok(42); let failure: Result<i32, &str> = Err("Something went wrong"); // Parsing returns Result let parsed = "42".parse::<i32>(); match parsed { Ok(num) => println!("Parsed: {}", num), Err(e) => println!("Error: {}", e), } // Result combinators let result = "42".parse::<i32>(); let doubled = result.map(|x| x * 2); println!("Doubled: {:?}", doubled); let handled = result.map_err(|e| format!("Parse error: {}", e)); println!("Handled: {:?}", handled); let unwrapped = result.unwrap_or(0); println!("Unwrapped or default: {}", unwrapped); // Working with multiple Results let nums = vec!["42", "24", "abc", "12"]; let parsed_nums: Vec<Result<i32, ParseIntError>> = nums .iter() .map(|s| s.parse()) .collect(); println!("Parsed results: {:?}", parsed_nums); // Filter out errors let valid_nums: Vec<i32> = nums .iter() .filter_map(|s| s.parse().ok()) .collect(); println!("Valid numbers: {:?}", valid_nums); } // Function returning Result fn divide(numerator: f64, denominator: f64) -> Result<f64, String> { if denominator == 0.0 { Err(String::from("Division by zero")) } else { Ok(numerator / denominator) } } // Using ? operator for error propagation fn calculate() -> Result<f64, String> { let a = divide(10.0, 2.0)?; let b = divide(a, 3.0)?; let c = divide(b, 0.0)?; // This will propagate error Ok(c) } 7. Reference Types
References and Borrowing
fn main() { let s = String::from("hello"); // Immutable reference let r1 = &s; let r2 = &s; // Multiple immutable references allowed println!("r1: {}, r2: {}", r1, r2); // Mutable reference let mut s2 = String::from("hello"); let r3 = &mut s2; r3.push_str(", world"); // let r4 = &s2; // Can't borrow immutably while mutably borrowed // Dangling references (prevented by compiler) // let ref_to_nothing = dangle(); // ERROR // Reference rules let mut data = String::from("data"); { let r1 = &data; // Immutable borrow let r2 = &data; // Another immutable borrow println!("r1: {}, r2: {}", r1, r2); // r1 and r2 go out of scope } let r3 = &mut data; // Now mutable borrow is allowed r3.push_str(" modified"); // Reference to reference let x = 5; let y = &x; let z = &&x; println!("{}", **z); // Dereferencing } // fn dangle() -> &String { // Returns reference to String // let s = String::from("hello"); // &s // s goes out of scope, reference would be dangling // } Smart Pointers
use std::rc::Rc; use std::cell::RefCell; use std::sync::{Arc, Mutex}; fn main() { // Box<T> - heap allocation let b = Box::new(5); println!("b = {}", b); // Box with recursive type #[derive(Debug)] enum List { Cons(i32, Box<List>), Nil, } let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("{:?}", list); // Rc<T> - reference counted (single-threaded) use List2::{Cons as Cons2, Nil as Nil2}; #[derive(Debug)] enum List2 { Cons(i32, Rc<List2>), Nil, } let a = Rc::new(Cons2(5, Rc::new(Cons2(10, Rc::new(Nil2))))); println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons2(3, Rc::clone(&a)); println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons2(4, Rc::clone(&a)); println!("count after creating c = {}", Rc::strong_count(&a)); } println!("count after c goes out of scope = {}", Rc::strong_count(&a)); // RefCell<T> - interior mutability let cell = RefCell::new(5); // Borrow mutably at runtime *cell.borrow_mut() += 1; println!("cell: {:?}", cell); // Multiple borrows checked at runtime { let r1 = cell.borrow(); let r2 = cell.borrow(); // let r3 = cell.borrow_mut(); // This would panic at runtime println!("r1: {}, r2: {}", r1, r2); } // Arc<T> + Mutex<T> for thread-safe sharing use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; })); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); } 8. Function Types
Function Pointers
fn add_one(x: i32) -> i32 { x + 1 } fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) } fn main() { // Function pointer let f: fn(i32) -> i32 = add_one; let result = f(5); println!("Result: {}", result); // Pass function as argument let result = do_twice(add_one, 5); println!("Twice: {}", result); // Function pointer from closure (no capture) let f2: fn(i32) -> i32 = |x| x + 1; println!("Closure as fn: {}", f2(5)); // Function pointers in collections let functions: Vec<fn(i32) -> i32> = vec![ add_one, |x| x * 2, |x| x * x, ]; for (i, f) in functions.iter().enumerate() { println!("Function {} result: {}", i, f(5)); } } // Function returning function pointer fn get_math_function(op: &str) -> Option<fn(i32, i32) -> i32> { match op { "add" => Some(|a, b| a + b), "sub" => Some(|a, b| a - b), "mul" => Some(|a, b| a * b), "div" => Some(|a, b| a / b), _ => None, } } 9. Type Aliases
// Type alias type Kilometers = i32; type Thunk = Box<dyn Fn() + Send + 'static>; fn main() { let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); // Type alias for complex types type Result<T> = std::result::Result<T, std::io::Error>; // Using in functions fn write_to_file() -> Result<()> { Ok(()) } // Type alias for closures type Callback = Box<dyn Fn(i32) -> i32>; let callbacks: Vec<Callback> = vec![ Box::new(|x| x * 2), Box::new(|x| x + 10), Box::new(|x| x * x), ]; for callback in callbacks { println!("Callback result: {}", callback(5)); } } // Type alias with generics type HashMap<K, V> = std::collections::HashMap<K, V>; // Usage in other module fn example() { let mut map: HashMap<String, i32> = HashMap::new(); map.insert("key".to_string(), 42); } 10. Never Type
#![allow(unused)] fn main() { // The never type ! represents computations that never complete // Functions that never return fn loop_forever() -> ! { loop { // infinite loop } } // panic! returns ! fn example() { let x = if true { 5 } else { panic!("This never happens"); // panic! returns !, so type checking works }; println!("x = {}", x); } // continue returns ! in loops let mut count = 0; loop { count += 1; if count < 5 { continue; // continue returns ! } break; } // The never type in match arms let value: Option<i32> = None; match value { Some(x) => println!("Got: {}", x), None => loop {}, // loop returns ! } // exit function returns ! use std::process; if false { process::exit(1); // This function returns ! } } 11. Custom Type Derivation
// Automatically implement common traits #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct Point { x: i32, y: i32, } #[derive(Debug, Clone, PartialEq)] enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, } fn main() { let p1 = Point { x: 10, y: 20 }; let p2 = p1; // Copy because of Copy trait let p3 = p1.clone(); // Clone println!("p1: {:?}", p1); println!("p1 == p2: {}", p1 == p2); let circle = Shape::Circle { radius: 5.0 }; let circle2 = circle.clone(); println!("circle: {:?}", circle); println!("circle == circle2: {}", circle == circle2); } // Default trait #[derive(Debug, Default)] struct Config { host: String, port: u16, timeout: Option<u64>, } fn default_example() { let config = Config::default(); println!("Default config: {:?}", config); let custom = Config { port: 8080, ..Default::default() }; } 12. Type Conversion
From and Into Traits
use std::convert::From; #[derive(Debug)] struct Number { value: i32, } // From trait impl From<i32> for Number { fn from(item: i32) -> Self { Number { value: item } } } // From for custom types struct Temperature { celsius: f64, } impl Temperature { fn from_fahrenheit(f: f64) -> Self { Temperature { celsius: (f - 32.0) * 5.0 / 9.0, } } fn from_kelvin(k: f64) -> Self { Temperature { celsius: k - 273.15, } } } fn main() { // Using From let num = Number::from(30); println!("Number from i32: {:?}", num); // Using Into (usually type annotation needed) let num2: Number = 30.into(); println!("Number from into: {:?}", num2); // Standard conversions let int_val = 5; let float_val = int_val as f64; let string_val = int_val.to_string(); let parsed_int = "42".parse::<i32>().unwrap(); // TryFrom for fallible conversions use std::convert::TryFrom; let large = 256; match i8::try_from(large) { Ok(val) => println!("Converted: {}", val), Err(e) => println!("Failed: {}", e), } // Custom conversions let boiling = Temperature::from_celsius(100.0); let freezing_f = Temperature::from_fahrenheit(32.0); let absolute_zero = Temperature::from_kelvin(0.0); } impl Temperature { fn from_celsius(c: f64) -> Self { Temperature { celsius: c } } fn to_fahrenheit(&self) -> f64 { self.celsius * 9.0 / 5.0 + 32.0 } fn to_kelvin(&self) -> f64 { self.celsius + 273.15 } } 13. Advanced Type Features
Associated Types
// Associated types in traits trait Container { type Item; fn add(&mut self, item: Self::Item); fn get(&self, index: usize) -> Option<&Self::Item>; } struct MyVec<T> { items: Vec<T>, } impl<T> Container for MyVec<T> { type Item = T; fn add(&mut self, item: T) { self.items.push(item); } fn get(&self, index: usize) -> Option<&T> { self.items.get(index) } } fn main() { let mut vec = MyVec { items: Vec::new() }; vec.add(42); vec.add(100); if let Some(val) = vec.get(0) { println!("First item: {}", val); } } Type Bounds and Where Clauses
use std::fmt::Display; // Generic function with type bounds fn print_and_return<T: Display>(item: T) -> T { println!("Item: {}", item); item } // Multiple bounds fn compare_and_print<T: PartialOrd + Display>(a: T, b: T) { if a > b { println!("{} is greater than {}", a, b); } else { println!("{} is less than or equal to {}", a, b); } } // Where clause for complex bounds fn complex_function<T, U>(t: T, u: U) -> i32 where T: Display + Clone, U: Clone + std::fmt::Debug, { println!("t: {}", t); println!("u: {:?}", u); 42 } // Trait objects trait Drawable { fn draw(&self); } struct Circle { radius: f64, } impl Drawable for Circle { fn draw(&self) { println!("Drawing circle with radius {}", self.radius); } } struct Square { side: f64, } impl Drawable for Square { fn draw(&self) { println!("Drawing square with side {}", self.side); } } fn draw_all(shapes: Vec<Box<dyn Drawable>>) { for shape in shapes { shape.draw(); } } fn main() { print_and_return(42); compare_and_print(10, 5); let shapes: Vec<Box<dyn Drawable>> = vec![ Box::new(Circle { radius: 5.0 }), Box::new(Square { side: 3.0 }), ]; draw_all(shapes); } Conclusion
Rust's type system provides a rich set of tools for creating safe, expressive, and efficient programs:
Key Takeaways
- Scalar Types: Integers, floats, booleans, and characters for basic values
- Compound Types: Tuples and arrays for grouping values
- String Types:
String(owned) and&str(borrowed) for text - Collection Types: Vectors for dynamic arrays
- Custom Types: Structs and enums for domain modeling
- Option and Result: For handling optional values and errors
- Reference Types: Borrowing and smart pointers for memory management
- Function Types: Function pointers and closures
- Type Aliases: For readability and abstraction
- Type Conversion: From/Into traits for safe conversions
Best Practices
- Use appropriate integer types (prefer
i32/u32unless size matters) - Use
f64as default floating-point type - Prefer
Stringfor owned text,&strfor string slices - Use enums for types with a fixed set of variants
- Derive common traits when appropriate
- Use type aliases for complex types
- Leverage the type system to prevent bugs at compile time
Rust's type system is one of its strongest features, providing compile-time guarantees that would be runtime checks in other languages, all without runtime overhead.