| name | rust-language |
| description | Guide for writing Rust code covering ownership, borrowing, lifetimes, error handling, async programming, and Rust best practices |
Rust Programming Language
This skill activates when writing Rust code, understanding ownership and borrowing, working with async Rust, or following Rust best practices.
When to Use This Skill
Activate when:
- Writing Rust code
- Understanding ownership, borrowing, and lifetimes
- Implementing error handling with Result and Option
- Working with traits and generics
- Writing async/concurrent Rust code
- Using Cargo and managing dependencies
- Following Rust idioms and best practices
Ownership and Borrowing
Ownership Rules
Rust's core concept - every value has exactly one owner:
// Ownership transfer (move)
let s1 = String::from("hello");
let s2 = s1; // s1 is no longer valid
// println!("{}", s1); // Error!
println!("{}", s2); // OK
// Copy types (stack-only data)
let x = 5;
let y = x; // x is still valid (Copy trait)
println!("{} {}", x, y); // OK
Borrowing
// Immutable borrow
fn calculate_length(s: &String) -> usize {
s.len()
}
let s = String::from("hello");
let len = calculate_length(&s);
println!("{} has length {}", s, len); // s still valid
// Mutable borrow
fn append_world(s: &mut String) {
s.push_str(" world");
}
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s); // "hello world"
Borrowing Rules
// Rule 1: Multiple immutable borrows OK
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2); // OK
// Rule 2: Only ONE mutable borrow at a time
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // Error!
println!("{}", r1);
// Rule 3: Cannot have mutable and immutable borrows together
let mut s = String::from("hello");
let r1 = &s;
// let r2 = &mut s; // Error!
println!("{}", r1);
Slices
// String slices
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
// Array slices
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..3]; // [2, 3]
// Function taking slice
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
Lifetimes
Lifetime Annotations
// Explicit lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
// Usage
let s1 = String::from("long string");
let s2 = String::from("short");
let result = longest(&s1, &s2);
println!("Longest: {}", result);
Lifetime in Structs
// Struct with lifetime
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return(&self) -> &str {
println!("Attention: {}", self.part);
self.part
}
}
// Usage
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt { part: first_sentence };
Lifetime Elision
// Compiler infers lifetimes (no annotation needed)
fn first_word(s: &str) -> &str {
// Compiler infers: fn first_word<'a>(s: &'a str) -> &'a str
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
Error Handling
Result Type
use std::fs::File;
use std::io::{self, Read};
// Returning Result
fn read_username_from_file() -> Result<String, io::Error> {
let mut file = File::open("username.txt")?;
let mut username = String::new();
file.read_to_string(&mut username)?;
Ok(username)
}
// Using Result
match read_username_from_file() {
Ok(username) => println!("Username: {}", username),
Err(e) => println!("Error: {}", e),
}
Option Type
// Option for optional values
fn find_user(id: u32) -> Option<User> {
if id == 1 {
Some(User { id: 1, name: "Alice".to_string() })
} else {
None
}
}
// Using Option
match find_user(1) {
Some(user) => println!("Found: {}", user.name),
None => println!("User not found"),
}
// Option combinators
let user = find_user(1)
.map(|u| u.name)
.unwrap_or("Unknown".to_string());
The ? Operator
// ? operator for error propagation
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // Returns early if Err
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
// Chaining with ?
fn process_file(path: &str) -> Result<usize, io::Error> {
let contents = read_file(path)?;
Ok(contents.len())
}
Custom Error Types
use std::fmt;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(std::num::ParseIntError),
Custom(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::Custom(s) => write!(f, "Error: {}", s),
}
}
}
impl From<io::Error> for AppError {
fn from(error: io::Error) -> Self {
AppError::Io(error)
}
}
// Usage
fn process() -> Result<(), AppError> {
let file = File::open("data.txt")?; // Auto-converts io::Error
// ...
Ok(())
}
Traits
Defining Traits
// Define a trait
trait Summary {
fn summarize(&self) -> String;
// Default implementation
fn summarize_author(&self) -> String {
String::from("(Read more...)")
}
}
// Implement trait
struct Article {
title: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, self.content)
}
}
Trait Bounds
// Function with trait bound
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Multiple trait bounds
fn process<T: Summary + Display>(item: &T) {
// ...
}
// Where clause (clearer for complex bounds)
fn complex<T, U>(t: &T, u: &U)
where
T: Summary + Clone,
U: Summary + Debug,
{
// ...
}
// impl Trait syntax
fn returns_summarizable() -> impl Summary {
Article {
title: String::from("Title"),
content: String::from("Content"),
}
}
Common Traits
// Clone and Copy
#[derive(Clone)]
struct Point {
x: i32,
y: i32,
}
// Debug
#[derive(Debug)]
struct User {
name: String,
age: u32,
}
// PartialEq and Eq
#[derive(PartialEq, Eq)]
struct Id(u32);
// PartialOrd and Ord
#[derive(PartialOrd, Ord, PartialEq, Eq)]
struct Priority(u32);
Generics
Generic Functions
// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
// Usage
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);
Generic Structs
// Generic struct
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point { x, y }
}
}
// Specific implementation for certain types
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
// Multiple type parameters
struct Pair<T, U> {
first: T,
second: U,
}
Generic Enums
// Option is a generic enum
enum Option<T> {
Some(T),
None,
}
// Result is a generic enum
enum Result<T, E> {
Ok(T),
Err(E),
}
Collections
Vectors
// Create vector
let mut v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3];
// Add elements
v.push(4);
v.push(5);
// Access elements
let third = &v[2]; // Panics if out of bounds
let third = v.get(2); // Returns Option<&T>
// Iterate
for i in &v {
println!("{}", i);
}
// Iterate and modify
for i in &mut v {
*i += 50;
}
HashMaps
use std::collections::HashMap;
// Create HashMap
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// Access values
let team = String::from("Blue");
let score = scores.get(&team); // Returns Option<&V>
// Iterate
for (key, value) in &scores {
println!("{}: {}", key, value);
}
// Update values
scores.entry(String::from("Blue")).or_insert(0);
*scores.entry(String::from("Blue")).or_insert(0) += 10;
Strings
// Create strings
let s = String::from("hello");
let s = "hello".to_string();
// Concatenation
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 is moved
// format! macro
let s = format!("{}-{}", "hello", "world");
// Iterate
for c in "hello".chars() {
println!("{}", c);
}
// Slicing (be careful with UTF-8!)
let hello = "Здравствуйте";
let s = &hello[0..4]; // "Зд"
Pattern Matching
Match Expressions
// Basic match
let number = 7;
match number {
1 => println!("One"),
2 | 3 | 5 | 7 | 11 => println!("Prime"),
13..=19 => println!("Teen"),
_ => println!("Other"),
}
// Match with destructuring
struct Point {
x: i32,
y: i32,
}
let p = Point { x: 0, y: 7 };
match p {
Point { x: 0, y } => println!("On y axis at {}", y),
Point { x, y: 0 } => println!("On x axis at {}", x),
Point { x, y } => println!("At ({}, {})", x, y),
}
If Let
// if let for simple matches
let some_value = Some(3);
if let Some(3) = some_value {
println!("three");
}
// With else
if let Some(x) = some_value {
println!("{}", x);
} else {
println!("None");
}
While Let
// while let for loops
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top);
}
Async Programming
Async Functions
use tokio;
// Async function
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
let response = reqwest::get(url).await?;
let body = response.text().await?;
Ok(body)
}
// Using async function
#[tokio::main]
async fn main() {
match fetch_data("https://example.com").await {
Ok(data) => println!("Data: {}", data),
Err(e) => println!("Error: {}", e),
}
}
Concurrent Async Operations
use tokio;
async fn fetch_multiple() {
// Sequential
let data1 = fetch_data("https://api1.com").await;
let data2 = fetch_data("https://api2.com").await;
// Concurrent with join!
let (data1, data2) = tokio::join!(
fetch_data("https://api1.com"),
fetch_data("https://api2.com")
);
// Concurrent with spawn
let handle1 = tokio::spawn(fetch_data("https://api1.com"));
let handle2 = tokio::spawn(fetch_data("https://api2.com"));
let data1 = handle1.await.unwrap();
let data2 = handle2.await.unwrap();
}
Streams
use tokio_stream::StreamExt;
async fn process_stream() {
let mut stream = tokio_stream::iter(vec![1, 2, 3, 4, 5]);
while let Some(value) = stream.next().await {
println!("Value: {}", value);
}
}
Concurrency
Threads
use std::thread;
use std::time::Duration;
// Spawn thread
let handle = thread::spawn(|| {
for i in 1..10 {
println!("Thread: {}", i);
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap();
// Move data into thread
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Vector: {:?}", v);
});
Channels
use std::sync::mpsc;
// Create channel
let (tx, rx) = mpsc::channel();
// Send from thread
thread::spawn(move || {
tx.send("hello").unwrap();
});
// Receive
let received = rx.recv().unwrap();
println!("Received: {}", received);
// Multiple senders
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || tx.send("from thread 1").unwrap());
thread::spawn(move || tx1.send("from thread 2").unwrap());
for received in rx {
println!("{}", received);
}
Shared State
use std::sync::{Arc, Mutex};
// Arc for shared ownership, Mutex for mutual exclusion
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
Testing
Unit Tests
// Tests in same file
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
#[test]
#[should_panic]
fn test_panic() {
panic!("This should panic");
}
#[test]
fn test_result() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
Integration Tests
// tests/integration_test.rs
use my_crate;
#[test]
fn test_integration() {
assert_eq!(my_crate::add(2, 2), 4);
}
Running Tests
# Run all tests
cargo test
# Run specific test
cargo test test_add
# Run with output
cargo test -- --nocapture
# Run integration tests only
cargo test --test integration_test
Cargo and Dependencies
Cargo.toml
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
reqwest = "0.11"
[dev-dependencies]
mockall = "0.11"
[profile.release]
opt-level = 3
lto = true
Common Commands
# Create new project
cargo new my_project
cargo new --lib my_lib
# Build
cargo build
cargo build --release
# Run
cargo run
cargo run --release
# Test
cargo test
# Check (faster than build)
cargo check
# Format code
cargo fmt
# Lint
cargo clippy
# Update dependencies
cargo update
# Add dependency
cargo add serde
Best Practices
Prefer Borrowing
// Good: Borrow when possible
fn process(data: &Vec<i32>) {
// Use data without taking ownership
}
// Avoid: Taking ownership unless needed
fn process(data: Vec<i32>) {
// Can't use data after calling this
}
Use ? for Error Propagation
// Good: Use ? operator
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
// Avoid: Manual match for each error
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
// ...
}
Use Iterators
// Good: Iterators (lazy, efficient)
let sum: i32 = vec![1, 2, 3, 4, 5]
.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * 2)
.sum();
// Avoid: Manual loops when iterators work
let mut sum = 0;
for x in vec![1, 2, 3, 4, 5] {
if x % 2 == 0 {
sum += x * 2;
}
}
Prefer &str over &String
// Good: Accept string slices
fn greet(name: &str) {
println!("Hello, {}", name);
}
// Can be called with both &str and &String
greet("Alice");
greet(&String::from("Bob"));
// Less flexible: Only accepts &String
fn greet(name: &String) {
println!("Hello, {}", name);
}
Common Patterns
Builder Pattern
#[derive(Default)]
struct User {
name: String,
email: String,
age: Option<u32>,
}
impl User {
fn builder() -> UserBuilder {
UserBuilder::default()
}
}
#[derive(Default)]
struct UserBuilder {
name: String,
email: String,
age: Option<u32>,
}
impl UserBuilder {
fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
fn email(mut self, email: impl Into<String>) -> Self {
self.email = email.into();
self
}
fn age(mut self, age: u32) -> Self {
self.age = Some(age);
self
}
fn build(self) -> User {
User {
name: self.name,
email: self.email,
age: self.age,
}
}
}
// Usage
let user = User::builder()
.name("Alice")
.email("alice@example.com")
.age(30)
.build();
Newtype Pattern
// Newtype for type safety
struct Meters(f64);
struct Seconds(f64);
fn calculate_speed(distance: Meters, time: Seconds) -> f64 {
distance.0 / time.0
}
// Can't accidentally swap parameters
let speed = calculate_speed(Meters(100.0), Seconds(9.8));
Key Principles
- Ownership ensures memory safety: No garbage collector needed
- Borrow checker prevents data races: Compile-time safety
- Zero-cost abstractions: High-level code compiles to efficient machine code
- Explicit over implicit: Be clear about ownership, mutability, errors
- Prefer immutability: Use
mutonly when needed - Use the type system: Let the compiler catch errors
- Test thoroughly: Tests are first-class in Rust
- Use clippy: Catch common mistakes and non-idiomatic code
Sources
See sources.md for documentation references used to create this skill.