Interactive Rust Cheatsheet
An interactive guide to Rust concepts, data structures, and concurrency patterns.
0. Rust Fundamentals
This section covers the foundational concepts of Rust programming, essential for building any application.
Packages & Imports (use)
Rust code is organized into crates and modules. Use use to bring items into scope.
use std::io; // Bring the `io` module into scope
use std::collections::HashMap; // Bring a specific struct into scope
fn main() {
println!("Hello, Rust!");
let mut map = HashMap::new();
map.insert("key", "value");
println!("{:?}", map);
}
Basic Data Types
Rust is statically typed and has several built-in types.
- Boolean:
bool(trueorfalse). - Numeric Types:
- Integers:
i8,i16,i32,i64,i128(signed);u8,u16,u32,u64,u128(unsigned);isize,usize(pointer-sized). - Floating-point:
f32,f64.
- Integers:
- Character:
char(Unicode scalar value, 4 bytes). - Tuple: Fixed-size collection of different types.
- Array: Fixed-size collection of same type.
- Slice: Dynamic-size view into a collection.
Variables & Mutability
Variables are immutable by default. Use mut to make them mutable.
fn main() {
let x = 5; // Immutable variable
println!("The value of x is: {}", x);
let mut y = 10; // Mutable variable
println!("The value of y is: {}", y);
y = 15;
println!("The new value of y is: {}", y);
const MAX_POINTS: u32 = 100_000; // Constants (must be annotated)
println!("Max points: {}", MAX_POINTS);
}
Shadowing
You can declare a new variable with the same name as a previous variable, shadowing it.
fn main() {
let x = 5;
let x = x + 1; // x is shadowed by a new x
let x = x * 2;
println!("The value of x is: {}", x); // Output: 12
}
Input/Output
Using std::io for console I/O.
use std::io;
fn main() {
println!("Hello, Rust!");
println!("Please enter your name:");
let mut name = String::new(); // Create a mutable, empty String
io::stdin()
.read_line(&mut name) // Read line into `name`
.expect("Failed to read line");
let name = name.trim(); // Shadow `name` with a trimmed version
println!("Hello, {}!", name);
}
Operators
Rust supports standard arithmetic, comparison, logical, and bitwise operators.
- **Arithmetic**:
+,-,*,/,% - **Comparison**:
==,!=,<,>,<=,>= - **Logical**:
&&(AND),||(OR),!(NOT) - **Bitwise**:
&,|,^,<<,>>
Control Flow
Statements that control the order of execution.
If-Else
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
// `if` is an expression
let condition = true;
let num = if condition { 5 } else { 6 };
println!("The value of num is: {}", num);
}
Match (Pattern Matching)
Powerful control flow operator for handling different cases of a value.
fn main() {
let coin = Coin::Quarter;
match coin {
Coin::Penny => println!("Lucky penny!"),
Coin::Nickel => println!("Five cents!"),
Coin::Dime => println!("Ten cents!"),
Coin::Quarter => println!("Twenty-five cents!"),
}
// Match with enum that holds data
let msg = Message::Write(String::from("hello"));
match msg {
Message::Quit => println!("The Quit variant has no data."),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to ({}, {}, {})", r, g, b),
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Loops
Rust has loop, while, and for loops.
fn main() {
// `loop` loop (infinite loop, can return a value)
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result); // Output: 20
// `while` loop
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!!!");
// `for` loop (iterating over collections)
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
// For loop with range
for number in (1..4).rev() { // 3, 2, 1
println!("{}!", number);
}
}
Functions
Functions are declared with the fn keyword. Return values are the last expression in the function body (without a semicolon).
fn main() {
another_function(5, 6);
let x = five();
println!("The value of x is: {}", x);
let y = plus_one(5);
println!("The value of y is: {}", y);
}
fn another_function(x: i32, y: i32) {
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
fn five() -> i32 {
5 // This is an expression, not a statement (no semicolon)
}
fn plus_one(x: i32) -> i32 {
x + 1 // This is an expression, not a statement
}
1. Ownership & Borrowing
Rust's core memory safety features. Ownership rules are checked at compile time.
Ownership Rules
- Each value in Rust has a variable that's called its *owner*.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped (memory freed).
fn main() {
let s1 = String::from("hello"); // s1 owns "hello"
let s2 = s1; // s1 is moved to s2; s1 is no longer valid
// println!("{}", s1); // Compile-time error: borrow of moved value: `s1`
let s3 = s2.clone(); // Deep copy, s2 and s3 are separate owners
println!("s2: {}, s3: {}", s2, s3);
takes_ownership(s2); // s2's value moves into the function, s2 is no longer valid
// println!("{}", s2); // Compile-time error
let x = 5; // Integers implement `Copy` trait, so they are copied
let y = x;
println!("x: {}, y: {}", x, y); // Both x and y are valid
makes_copy(x); // x's value is copied into the function
println!("x: {}", x); // x is still valid
}
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // some_string goes out of scope and `drop` is called. Memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // some_integer goes out of scope. Nothing special happens.
Borrowing (References)
- Allows you to use values without taking ownership.
- References are immutable by default. Use
&mutfor mutable references.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Pass a reference to s1
println!("The length of '{}' is {}.", s1, len); // s1 is still valid
let mut s = String::from("hello");
change(&mut s); // Pass a mutable reference
println!("Modified string: {}", s);
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // s goes out of scope. Nothing is dropped.
fn change(some_string: &mut String) { // some_string is a mutable reference
some_string.push_str(", world");
}
Rules of References
- At any given time, you can have *either* one mutable reference *or* any number of immutable references.
- References must always be valid (no dangling references).
fn main() {
let mut s = String::from("hello");
let r1 = &s; // Immutable reference
let r2 = &s; // Another immutable reference
println!("{}, {}", r1, r2);
// r1 and r2 go out of scope here, so we can create a mutable reference below
let r3 = &mut s; // One mutable reference
r3.push_str(" world");
println!("{}", r3);
// This would be a compile-time error:
// let r4 = &s; // Cannot have immutable reference while mutable reference (r3) is active
// println!("{}", r4);
}
Slices
References to contiguous sequence of elements in a collection, without taking ownership.
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // Slice from index 0 to 5 (exclusive)
let world = &s[6..11]; // Slice from index 6 to 11 (exclusive)
println!("{} {}", hello, world);
let full_slice = &s[..]; // Slice of the entire string
println!("{}", full_slice);
let mut a = [1, 2, 3, 4, 5];
let slice = &mut a[1..4]; // Mutable slice of an array
slice[0] = 99;
println!("{:?}", a); // Output: [1, 99, 3, 4, 5]
}
2. Data Structures
Rust's standard library provides robust and efficient data structures.
Vectors (Vec<T>)
- Description: A growable list of values of the same type. Stored contiguously in memory.
- Performance: Access by index: $O(1)$, Push/Pop (end): $O(1)$ amortized, Insert/Delete (middle/beginning): $O(N)$.
- Thread Safety: Not inherently thread-safe for concurrent mutable access. Use
Arc<Mutex<Vec<T>>>for shared mutable state.
fn main() {
let mut v: Vec<i32> = Vec::new(); // Create an empty vector
v.push(5);
v.push(6);
v.push(7);
println!("{:?}", v); // Output: [5, 6, 7]
let v2 = vec![1, 2, 3]; // Macro for creating a vector with initial values
println!("{:?}", v2);
let third: &i32 = &v[2]; // Access element by index
println!("The third element is {}", third);
// Iterating
for i in &mut v {
*i += 50; // Dereference to modify value
}
println!("{:?}", v);
}
Strings (String and &str)
- Description:
Stringis a growable, heap-allocated, UTF-8 encoded string.&stris a string slice (immutable view) into aStringor string literal. - Performance:
Stringappend: $O(1)$ amortized, Concatenation: $O(N)$.&stroperations: $O(1)$ (slice creation), $O(N)$ (iteration). - Thread Safety:
Stringis not inherently thread-safe for concurrent mutable access.&str(immutable) is safe to share.
fn main() {
let mut s = String::new(); // Empty mutable String
s.push_str("hello"); // Append a string slice
s.push(' '); // Append a character
s.push_str("world");
println!("{}", s); // Output: hello world
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3); // Efficient concatenation
println!("{}", s);
let hello = "Здравствуйте"; // String literal (&str)
for c in hello.chars() { // Iterate over Unicode characters
print!("{} ", c);
}
println!();
}
Hash Maps (HashMap<K, V>)
- Description: Stores key-value pairs using a hash table. Keys must be unique.
- Performance: Insert/Lookup/Delete: $O(1)$ on average, $O(N)$ worst case (hash collisions).
- Thread Safety: Not inherently thread-safe for concurrent mutable access. Use
Arc<Mutex<HashMap<K, V>>>orDashMap(a concurrent hash map crate).
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
println!("{:?}", scores);
let team_name = String::from("Blue");
let score = scores.get(&team_name); // Get a reference to the value
println!("Blue team score: {:?}", score);
// Iterate over map
for (key, value) in &scores {
println!("{}: {}", key, value);
}
// Insert only if key not present
scores.entry(String::from("Blue")).or_insert(25);
scores.entry(String::from("Green")).or_insert(25);
println!("{:?}", scores);
}
Structs
- Description: Custom data types that let you name and package together multiple related values.
- Performance: Accessing fields: $O(1)$.
- Thread Safety: Fields are not inherently thread-safe.
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
println!("User: {} ({})", user1.username, user1.email);
let mut user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
active: true,
sign_in_count: 1,
};
user2.email = String::from("newemail@example.com"); // Mutate field
println!("User 2 new email: {}", user2.email);
// Struct Update Syntax
let user3 = User {
email: String::from("third@example.com"),
..user2 // Take remaining fields from user2 (moves non-Copy types)
};
// println!("{}", user2.username); // Error: user2.username moved to user3
println!("User 3 username: {}", user3.username);
}
Enums
- Description: Allow you to define a type by enumerating its possible variants. Variants can optionally hold data.
- Performance: Pattern matching is highly optimized by the compiler.
- Thread Safety: Enums themselves are safe. Data held within enum variants follows standard ownership/borrowing rules.
enum IpAddrKind {
V4,
V6,
}
enum IpAddr { // Enum with data
V4(u8, u8, u8, u8),
V6(String),
}
enum Message { // More complex enum variants
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
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 msg = Message::Write(String::from("hello"));
// Use match to handle enum variants (see Control Flow section for example)
}
Comparison of Rust Built-in Data Structures
| Data Structure | Description | Fixed/Dynamic Size | Access (Index/Key) | Insertion (Avg) | Deletion (Avg) | Ordered | Unique Elements | Thread Safety (Concurrent Mod.) |
|---|---|---|---|---|---|---|---|---|
Array | Fixed-size sequence of elements | Fixed | $O(1)$ | N/A | N/A | Yes (index order) | No | No (requires external sync) |
Vec<T> | Growable list (dynamic array) | Dynamic | $O(1)$ | $O(1)$ amortized (push), $O(N)$ (insert middle) | $O(N)$ | Yes (insertion order) | No | No (requires external sync) |
String | Growable, heap-allocated UTF-8 text | Dynamic | $O(1)$ (byte index), $O(N)$ (char index) | $O(1)$ amortized (push_str) | $O(N)$ | Yes (byte order) | N/A | No (requires external sync) |
&str | Immutable string slice | Fixed (view) | $O(1)$ (byte index), $O(N)$ (char index) | N/A | N/A | Yes (byte order) | N/A | Yes (immutable) |
HashMap<K, V> | Unordered key-value pairs (Hash Table) | Dynamic | $O(1)$ (avg), $O(N)$ (worst) | $O(1)$ (avg), $O(N)$ (worst) | $O(1)$ (avg), $O(N)$ (worst) | No | Keys: Yes | No (requires external sync) |
Struct | Custom composite data type | Fixed (fields) | $O(1)$ (field access) | N/A | N/A | Yes (field declaration order) | N/A | Fields not inherently safe (requires external sync) |
Enum | Type with enumerated variants | N/A | N/A | N/A | N/A | N/A | N/A | Variants follow data rules |
3. Concurrency (Threads, Channels, Shared State)
Rust's concurrency model emphasizes safety through its ownership system, preventing data races at compile time.
Threads (std::thread)
Create new OS threads using thread::spawn.
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| { // Closure runs in new thread
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap(); // Wait for the spawned thread to finish
println!("Main thread finished.");
}
Message Passing (Channels - std::sync::mpsc)
Communicating between threads safely using Multiple Producer, Single Consumer (MPSC) channels.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel(); // Create a new channel: (transmitter, receiver)
thread::spawn(move || { // Move tx into the spawned thread
let val = String::from("hi");
tx.send(val).unwrap(); // Send value through the channel
// println!("val is {}", val); // Error: val moved to tx.send()
});
let received = rx.recv().unwrap(); // Block until a value is received
println!("Got: {}", received);
// Multiple messages
let (tx2, rx2) = mpsc::channel();
let tx3 = mpsc::Sender::clone(&tx2); // Clone transmitter for multiple producers
thread::spawn(move || {
let msgs = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for msg in msgs {
tx2.send(msg).unwrap();
thread::sleep(Duration::from_millis(100));
}
});
thread::spawn(move || {
let msgs = vec![
String::from("and"),
String::from("even"),
String::from("more"),
String::from("messages"),
];
for msg in msgs {
tx3.send(msg).unwrap();
thread::sleep(Duration::from_millis(50));
}
});
for received in rx2 { // rx2 acts as an iterator
println!("Got: {}", received);
}
}
Shared State Concurrency (Mutex<T> and Arc<T>)
Protecting shared mutable data with a mutex, shared across threads using an atomic reference counter.
use std::sync::{Mutex, Arc};
use std::thread;
fn main() {
// Arc enables multiple ownership over shared data
// Mutex provides mutual exclusion for safe mutable access
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter); // Clone Arc for each thread
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap(); // Acquire lock, blocks if already locked
*num += 1; // Mutate the protected data
// Lock is automatically released when `num` goes out of scope
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap()); // Final count should be 10
}
4. Traits
Traits define shared behavior in an abstract way. They are similar to interfaces in other languages.
Defining a Trait
pub trait Summary {
fn summarize(&self) -> String; // Required method
// Default implementation (optional)
fn summarize_author(&self) -> String {
String::from("(Read more...)")
}
}
Implementing a Trait for a Type
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("Tweet summary: {}", tweet.summarize());
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup!"),
location: String::from("Pittsburgh, PA"),
author: String::from("Iceburgh"),
content: String::from("The Pittsburgh Penguins once again won the Stanley Cup."),
};
println!("Article summary: {}", article.summarize());
println!("Article author summary: {}", article.summarize_author()); // Using default implementation
}
// Trait as a parameter (Trait Bound Syntax)
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// Trait as a parameter (impl Trait syntax - syntactic sugar for trait bounds)
pub fn notify_sugar(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// Trait as a return type (only if returning single concrete type)
// pub fn returns_summarizable() -> impl Summary {
// Tweet { /* ... */ }
// }
Trait Objects (Dynamic Dispatch)
Allows working with values of different types that implement the same trait, using runtime polymorphism.
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>, // Vector of trait objects
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
println!("Drawing a Button ({}x{}) with label: {}", self.width, self.height, self.label);
}
}
pub struct SelectBox {
pub width: u32,
pub height: u32,
pub options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
println!("Drawing a SelectBox ({}x{}) with options: {:?}", self.width, self.height, self.options);
}
}
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 20,
label: String::from("OK"),
}),
],
};
screen.run();
}
5. Error Handling (Result & Option)
Rust emphasizes explicit error handling using enums, rather than exceptions.
panic! (Unrecoverable Errors)
Used for unrecoverable errors, typically indicating a bug in your code.
fn main() {
// panic!("crash and burn"); // This will cause the program to crash
let v = vec![1, 2, 3];
// v[99]; // This would panic at runtime if not caught by bounds checks
}
Option<T> (Absence of a Value)
An enum that represents the possibility of a value being present or absent. Used for situations where a value might or might not exist.
fn main() {
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
// Using `match` with Option
let x = 5;
let y: Option<i32> = Some(5);
match y {
Some(i) => println!("Value is: {}", i),
None => println!("No value"),
}
// Using `if let` for concise matching
if let Some(value) = some_number {
println!("The value is: {}", value);
} else {
println!("No value present.");
}
// `unwrap()` and `expect()` (use with caution, can panic!)
let value = some_number.unwrap(); // Panics if None
let value = some_string.expect("String should be present"); // Panics with custom message if None
}
Result<T, E> (Recoverable Errors)
An enum that represents the possibility of either success (Ok(T)) or failure (Err(E)). Used for operations that might fail in a way you want to handle.
use std::fs::File;
use std::io::ErrorKind; // For specific error kinds
fn main() {
let f = File::open("hello.txt"); // Returns a Result<File, io::Error>
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
println!("File opened/created successfully: {:?}", f);
// Using `unwrap()` and `expect()` with Result (use with caution!)
// let f = File::open("another.txt").unwrap(); // Panics on Err
// let f = File::open("another.txt").expect("Failed to open another.txt"); // Panics with custom message on Err
// Using `?` operator for propagating errors
// (Can only be used in functions that return Result)
// fn read_username_from_file() -> Result<String, io::Error> {
// let mut f = File::open("username.txt")?; // Propagates error if any
// let mut s = String::new();
// f.read_to_string(&mut s)?; // Propagates error if any
// Ok(s)
// }
}
6. Lifetimes
Lifetimes are a Rust concept that ensures references are always valid. They are a compile-time concept and don't affect runtime performance.
- **Purpose**: Prevent dangling references (references that point to invalid memory).
- **Syntax**: Lifetime annotations start with an apostrophe (
'), e.g.,'a. - **Lifetime Elision Rules**: The compiler can often infer lifetimes, so you don't always need to explicitly write them.
Function Lifetimes
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
let string3 = String::from("long string is long");
{
let string4 = String::from("xyz");
let result = longest(string3.as_str(), string4.as_str());
println!("The longest string is {}", result);
}
}
// Function signature with lifetime annotations
// 'a indicates that the returned reference will live as long as the shortest of the two input references.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Struct Lifetimes
If a struct holds references, you must specify lifetime parameters for those references.
struct ImportantExcerpt<'a> {
part: &'a str, // This struct holds a reference with lifetime 'a
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt { part: first_sentence };
println!("Excerpt: {}", i.part);
}
7. Modules & Crates
Rust's code organization system for managing projects and dependencies.
- **Crate**: The smallest unit of code that the Rust compiler considers. Can be a binary (executable) or a library.
- **Module**: Organizes code within a crate for readability and reuse. Defines privacy (public/private).
- **
Cargo.toml**: The manifest file for a Rust project, defining dependencies and metadata. - **
cargo new**: Creates a new Rust project (crate). - **
cargo build**: Compiles the project. - **
cargo run**: Compiles and runs the project. - **
cargo test**: Runs tests.
Module Example
// src/main.rs
mod front_of_house { // Define a module
pub mod hosting { // Public submodule
pub fn add_to_waitlist() { // Public function
println!("Added to waitlist!");
}
}
mod serving { // Private submodule
fn take_order() {}
}
}
fn main() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
// Using `use` to bring into scope
use crate::front_of_house::hosting;
hosting::add_to_waitlist();
}
Adding a Dependency (Cargo.toml)
# Cargo.toml
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8.5" # Example dependency
Then, in your Rust code:
use rand::Rng; // Use a function from the 'rand' crate
fn main() {
let secret_number = rand::thread_rng().gen_range(1..101);
println!("Secret number: {}", secret_number);
}
8. Testing
Rust has a built-in testing framework. Test code lives in functions annotated with #[test].
- **Unit Tests**: Typically placed in the same file as the code they're testing, within a
mod testsmodule annotated with#[cfg(test)]. - **Integration Tests**: Placed in the
testsdirectory at the root of your project.
// src/lib.rs (or src/main.rs for unit tests)
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)] // Only compile when running tests
mod tests {
use super::*; // Bring outer items into scope
#[test] // Marks a function as a test
fn it_works() {
assert_eq!(4, add_two(2)); // Assertion macro
}
#[test]
#[should_panic(expected = "less than or equal to 100")] // Expect a panic with specific message
fn another_test() {
// This test will pass if the code inside panics with the expected message
panic_if_too_high(101);
}
fn panic_if_too_high(x: i32) {
if x > 100 {
panic!("Value {} is too high, must be less than or equal to 100", x);
}
}
#[test]
#[ignore = "reason for ignoring"] // Ignores this test by default
fn expensive_test() {
// This test won't run with `cargo test`
// Run with `cargo test -- --ignored`
}
}
// To run tests:
// cargo test
// cargo test -- --show-output // Show println! output from tests
// cargo test it_works // Run specific test
// cargo test -- --test-threads=1 // Run tests sequentially
9. Memory Management (Ownership, Borrowing, Drop Trait)
Rust's memory management is handled by its ownership system at compile time, eliminating garbage collectors and manual memory deallocation.
- **Ownership**: (See section 1) Each value has a single owner. When the owner goes out of scope, the value is dropped.
- **Borrowing**: (See section 1) References allow temporary access to owned data without transferring ownership.
- **Drop Trait**: Allows you to customize what happens when a value is about to go out of scope.
Drop Trait
Implement the Drop trait to define custom cleanup logic for your types.
struct CustomSmartPointer {
data: String,
}
// Implement the Drop trait for CustomSmartPointer
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
// 'c' and 'd' will be dropped when they go out of scope,
// calling their `drop` implementations automatically.
// You cannot explicitly call `drop()` directly to clean up:
// c.drop(); // Compile-time error: explicit use of destructor method
// Use `std::mem::drop` if you need to force an early drop:
// std::mem::drop(c);
// println!("CustomSmartPointer c dropped before the end of main.");
}
10. Design Patterns
Common solutions to recurring problems in software design, adapted for Rust's unique features like ownership, traits, and enums.
Singleton ▾
Ensures a class has only one instance and provides a global point of access to it. In Rust, this often involves lazy_static or once_cell crates for thread-safe lazy initialization.
use lazy_static::lazy_static; // Requires `lazy_static = "1.4.0"` in Cargo.toml
use std::sync::Mutex;
struct AppConfig {
pub setting_a: String,
pub setting_b: u32,
}
lazy_static! {
static ref CONFIG: Mutex<AppConfig> = Mutex::new(AppConfig {
setting_a: String::from("default_a"),
setting_b: 100,
});
}
impl AppConfig {
pub fn get_instance() -> &'static Mutex<AppConfig> {
&CONFIG
}
pub fn show_settings(&self) {
println!("Setting A: {}, Setting B: {}", self.setting_a, self.setting_b);
}
}
fn main() {
let config_instance = AppConfig::get_instance();
let locked_config = config_instance.lock().unwrap();
locked_config.show_settings();
drop(locked_config); // Explicitly release lock
// Modify settings (requires mutable access)
let mut mutable_config = config_instance.lock().unwrap();
mutable_config.setting_a = String::from("updated_a");
mutable_config.setting_b = 200;
drop(mutable_config);
let final_config = config_instance.lock().unwrap();
final_config.show_settings();
}
Factory Method ▾
Provides an interface for creating objects, allowing different implementations to decide which concrete type to instantiate.
trait Product {
fn get_name(&self) -> String;
}
struct ConcreteProductA;
impl Product for ConcreteProductA {
fn get_name(&self) -> String {
"Product A".to_string()
}
}
struct ConcreteProductB;
impl Product for ConcreteProductB {
fn get_name(&self) -> String {
"Product B".to_string()
}
}
trait Creator {
fn create_product(&self) -> Box<dyn Product>; // Returns a trait object
}
struct ConcreteCreatorA;
impl Creator for ConcreteCreatorA {
fn create_product(&self) -> Box<dyn Product> {
Box::new(ConcreteProductA)
}
}
struct ConcreteCreatorB;
impl Creator for ConcreteCreatorB {
fn create_product(&self) -> Box<dyn Product> {
Box::new(ConcreteProductB)
}
}
fn main() {
let creator_a = ConcreteCreatorA;
let product_a = creator_a.create_product();
println!("Created: {}", product_a.get_name());
let creator_b = ConcreteCreatorB;
let product_b = creator_b.create_product();
println!("Created: {}", product_b.get_name());
}
Observer ▾
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Often implemented using channels or shared state with mutexes in Rust.
use std::sync::{Arc, Mutex};
trait Observer {
fn update(&self, message: &str);
}
struct Subject {
observers: Mutex<Vec<Arc<dyn Observer + Send + Sync>>>, // Observers are trait objects
}
impl Subject {
fn new() -> Self {
Subject {
observers: Mutex::new(Vec::new()),
}
}
fn attach(&self, observer: Arc<dyn Observer + Send + Sync>) {
self.observers.lock().unwrap().push(observer);
}
fn detach(&self, observer_to_remove: Arc<dyn Observer + Send + Sync>) {
let mut observers = self.observers.lock().unwrap();
observers.retain(|obs| !Arc::ptr_eq(obs, &observer_to_remove));
}
fn notify_observers(&self, message: &str) {
for observer in self.observers.lock().unwrap().iter() {
observer.update(message);
}
}
}
struct ConcreteObserver {
name: String,
}
impl Observer for ConcreteObserver {
fn update(&self, message: &str) {
println!("{} received update: {}", self.name, message);
}
}
fn main() {
let subject = Subject::new();
let obs1 = Arc::new(ConcreteObserver { name: "Observer 1".to_string() });
let obs2 = Arc::new(ConcreteObserver { name: "Observer 2".to_string() });
subject.attach(Arc::clone(&obs1));
subject.attach(Arc::clone(&obs2));
subject.notify_observers("A new event occurred!");
subject.detach(obs1); // Detach obs1
subject.notify_observers("Another event!");
}
Strategy ▾
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
trait PaymentStrategy {
fn pay(&self, amount: u32);
}
struct CreditCardPayment;
impl PaymentStrategy for CreditCardPayment {
fn pay(&self, amount: u32) {
println!("Paying {} using Credit Card.", amount);
}
}
struct PayPalPayment;
impl PaymentStrategy for PayPalPayment {
fn pay(&self, amount: u32) {
println!("Paying {} using PayPal.", amount);
}
}
struct ShoppingCart {
strategy: Box<dyn PaymentStrategy>, // Stores a trait object
}
impl ShoppingCart {
fn new(strategy: Box<dyn PaymentStrategy>) -> Self {
ShoppingCart { strategy }
}
fn set_payment_strategy(&mut self, strategy: Box<dyn PaymentStrategy>) {
self.strategy = strategy;
}
fn checkout(&self, amount: u32) {
self.strategy.pay(amount);
}
}
fn main() {
let mut cart = ShoppingCart::new(Box::new(CreditCardPayment));
cart.checkout(100);
cart.set_payment_strategy(Box::new(PayPalPayment));
cart.checkout(50);
}
Decorator ▾
Attaches additional responsibilities to an object dynamically.
trait Coffee {
fn get_cost(&self) -> f64;
fn get_ingredients(&self) -> String;
}
struct SimpleCoffee;
impl Coffee for SimpleCoffee {
fn get_cost(&self) -> f64 { 5.0 }
fn get_ingredients(&self) -> String { "Coffee".to_string() }
}
// Decorator struct (holds a Box<dyn Coffee>)
struct CoffeeDecorator {
decorated_coffee: Box<dyn Coffee>,
}
// Concrete Decorators
struct MilkDecorator {
base: CoffeeDecorator,
}
impl Coffee for MilkDecorator {
fn get_cost(&self) -> f64 { self.base.decorated_coffee.get_cost() + 1.5 }
fn get_ingredients(&self) -> String {
format!("{}, Milk", self.base.decorated_coffee.get_ingredients())
}
}
struct SugarDecorator {
base: CoffeeDecorator,
}
impl Coffee for SugarDecorator {
fn get_cost(&self) -> f64 { self.base.decorated_coffee.get_cost() + 0.5 }
fn get_ingredients(&self) -> String {
format!("{}, Sugar", self.base.decorated_coffee.get_ingredients())
}
}
fn main() {
let mut my_coffee: Box<dyn Coffee> = Box::new(SimpleCoffee);
println!("Cost: {:.2}, Ingredients: {}", my_coffee.get_cost(), my_coffee.get_ingredients());
my_coffee = Box::new(MilkDecorator { base: CoffeeDecorator { decorated_coffee: my_coffee } });
println!("Cost: {:.2}, Ingredients: {}", my_coffee.get_cost(), my_coffee.get_ingredients());
my_coffee = Box::new(SugarDecorator { base: CoffeeDecorator { decorated_coffee: my_coffee } });
println!("Cost: {:.2}, Ingredients: {}", my_coffee.get_cost(), my_coffee.get_ingredients());
}
Visitor ▾
Represents an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
trait Visitor {
fn visit_concrete_element_a(&self, element: &ConcreteElementA);
fn visit_concrete_element_b(&self, element: &ConcreteElementB);
}
trait Element {
fn accept(&self, visitor: &dyn Visitor);
}
struct ConcreteElementA;
impl ConcreteElementA {
fn operation_a(&self) -> String { "ConcreteElementA".to_string() }
}
impl Element for ConcreteElementA {
fn accept(&self, visitor: &dyn Visitor) { visitor.visit_concrete_element_a(self); }
}
struct ConcreteElementB;
impl ConcreteElementB {
fn operation_b(&self) -> String { "ConcreteElementB".to_string() }
}
impl Element for ConcreteElementB {
fn accept(&self, visitor: &dyn Visitor) { visitor.visit_concrete_element_b(self); }
}
struct ConcreteVisitor1;
impl Visitor for ConcreteVisitor1 {
fn visit_concrete_element_a(&self, element: &ConcreteElementA) {
println!("Visitor 1 processing {}", element.operation_a());
}
fn visit_concrete_element_b(&self, element: &ConcreteElementB) {
println!("Visitor 1 processing {}", element.operation_b());
}
}
struct ConcreteVisitor2;
impl Visitor for ConcreteVisitor2 {
fn visit_concrete_element_a(&self, element: &ConcreteElementA) {
println!("Visitor 2 processing {} differently.", element.operation_a());
}
fn visit_concrete_element_b(&self, element: &ConcreteElementB) {
println!("Visitor 2 processing {} differently.", element.operation_b());
}
}
fn main() {
let elements: Vec<Box<dyn Element>> = vec![
Box::new(ConcreteElementA),
Box::new(ConcreteElementB),
];
let visitor1 = ConcreteVisitor1;
for elem in &elements {
elem.accept(&visitor1);
}
let visitor2 = ConcreteVisitor2;
for elem in &elements {
elem.accept(&visitor2);
}
}