Essential Rust Concepts: Ownership, Structs, Enums, and Error Handling
Ownership in Rust
Ownership is a core concept in Rust that ensures memory safety without a garbage collector. It revolves around three key rules:
- Each value has a single owner.
- When the owner goes out of scope, the value is dropped.
- References allow borrowing without transferring ownership.
Example illustrating ownership and borrowing:
fn modify_value(data: &mut i32) {
let local_val = 5;
let stack_ref = &local_val;
let heap_val = Box::new(10);
let heap_ref = &heap_val;
let deref_heap = &*heap_val;
*data += 5;
}
fn main() {
let mut number = 0;
modify_value(&mut number);
}
Structs for Organizing Data
Structs group related data in to custom types.
Defining and Instantiating Structs
Define a struct:
struct UserProfile {
active: bool,
username: String,
email: String,
login_count: u64,
}
Create an instance:
let profile1 = UserProfile {
active: true,
username: String::from("user123"),
email: String::from("user@example.com"),
login_count: 1,
};
Field init shorthand simplifies assignment when variable names match field names.
let username = String::from("user456");
let profile2 = UserProfile {
username,
active: false,
email: String::from("another@example.com"),
login_count: 0,
};
Struct update syntax creates a new instance based on an existing one.
let profile3 = UserProfile {
email: String::from("new@example.com"),
..profile1
};
Tuple Structs and Unit Structs
Tuple structs have named types without named fields.
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
Unit structs have no fields.
struct Marker;
let marker = Marker;
Methods and Associated Functions
Define methods within impl blocks.
impl UserProfile {
fn is_active(&self) -> bool {
self.active
}
fn increment_login(&mut self) {
self.login_count += 1;
}
fn create_default() -> Self {
Self {
active: true,
username: String::from("default"),
email: String::from("default@example.com"),
login_count: 0,
}
}
}
Enums and Pattern Matching
Enums define types that can be one of several variants.
enum Transaction {
Withdraw(f64),
Deposit { amount: f64, account: String },
Transfer,
}
The Option Enum
Rust uses Option<T> to handle absence of values safely.
let present_value: Option<i32> = Some(42);
let absent_value: Option<i32> = None;
Pattern Matchign with match
match allows exhaustive handling of enum variants.
fn process_transaction(tx: Transaction) -> String {
match tx {
Transaction::Withdraw(amt) => format!("Withdrawn: {}", amt),
Transaction::Deposit { amount, account } => format!("Deposited {} to {}", amount, account),
Transaction::Transfer => String::from("Transfer initiated"),
}
}
Use if let for concise single-pattern matching.
if let Transaction::Withdraw(amt) = tx {
println!("Withdrawal amount: {}", amt);
}
Error Handling
Rust distinguishes between recoverable errors with Result and unrecoverable errors with panic!.
Recoverable Errors with Result
Result<T, E> represents either success (Ok(T)) or failure (Err(E)).
use std::fs::File;
use std::io::{self, Read};
fn read_config() -> Result<String, io::Error> {
let mut file = File::open("config.txt")?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
The ? operator propagates errors concisely.
Unrecoverable Errors with panic!
Use panic! to unrecoverable errors.
fn validate_input(value: i32) {
if value < 0 {
panic!("Input must be non-negative");
}
}
Collectionns
Vectors
Vectors store multiple values of the same type in a heap-allocated array.
let mut numbers: Vec<i32> = Vec::new();
numbers.push(10);
numbers.push(20);
let first = numbers[0];
let second = numbers.get(1);
for num in &numbers {
println!("{}", num);
}
Strings
String is a growable, UTF-8 encoded text type.
let mut greeting = String::from("Hello");
greeting.push_str(", world!");
let combined = format!("{}{}", greeting, " Welcome");
Hash Maps
HashMap<K, V> stores key-value pairs with unique keys.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 20);
let blue_score = scores.get("Blue");
for (team, score) in &scores {
println!("{}: {}", team, score);
}
Generics, Traits, and Lifetimse
Generics
Generics allow writing code that works with multiple types.
fn find_largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
Traits
Traits define shared behavior across types.
trait Summarizable {
fn summary(&self) -> String;
}
struct Article {
title: String,
content: String,
}
impl Summarizable for Article {
fn summary(&self) -> String {
format!("{}: {}", self.title, &self.content[..50])
}
}
Lifetimes
Lifetimes ensure references remain valid.
fn longest_string<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() { s1 } else { s2 }
}
Testing
Write tests using the #[test] attribute.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn addition_works() {
assert_eq!(2 + 2, 4);
}
#[test]
#[should_panic]
fn invalid_input_panics() {
validate_input(-5);
}
}
Run tests with cargo test.