Table of Contents
- Understand Ownership and Borrowing
- Embrace Enums and Pattern Matching
- Handle Errors Gracefully with
ResultandOption - Prefer Iterators Over Loops
- Organize Code with Modules and Crates
- Follow Naming Conventions
- Leverage the Standard Library
- Document and Communicate Clearly
- Optimize Without Sacrificing Safety
- Conclusion
- References
1. Understand Ownership and Borrowing
Rust’s ownership model is its most distinctive feature, and idiomatic code leans into it rather than fighting it. Ownership ensures memory safety without garbage collection, but to use it effectively, you must minimize unnecessary copies and leverage borrowing.
Key Practices:
- Avoid unnecessary
clone(): Cloning data (e.g.,String,Vec) is expensive. Prefer borrowing with&when you don’t need ownership. - Use
Cowfor clone-on-write: When you might need to modify borrowed data,Cow<'a, T>(short for “clone on write”) lets you borrow immutably or clone only when modification is needed. - Prefer slices over owned types for function parameters: Accept
&strinstead ofString, or&[T]instead ofVec<T>, to make functions more flexible.
Example: Borrowing Instead of Cloning
// Non-idiomatic: Unnecessary clone
fn print_name(name: String) {
println!("Name: {}", name);
}
let my_name = String::from("Alice");
print_name(my_name.clone()); // Cloning here is wasteful!
println!("My name is still: {}", my_name); // my_name is still owned, but clone is unnecessary
// Idiomatic: Borrow with &str
fn print_name_idiomatic(name: &str) {
println!("Name: {}", name);
}
let my_name = String::from("Alice");
print_name_idiomatic(&my_name); // No clone needed!
println!("My name is still: {}", my_name); // my_name remains owned
Example: Using Cow
use std::borrow::Cow;
fn process_data(input: Cow<'_, str>) -> Cow<'_, str> {
if input.starts_with('$') {
// Modify: clone the borrowed data into an owned String
let mut owned = input.into_owned();
owned.push_str("_processed");
Cow::Owned(owned)
} else {
// No modification: return the borrowed data
input
}
}
let borrowed = "hello";
let owned = String::from("$world");
assert_eq!(process_data(Cow::Borrowed(borrowed)), Cow::Borrowed("hello"));
assert_eq!(process_data(Cow::Owned(owned)), Cow::Owned(String::from("$world_processed")));
2. Embrace Enums and Pattern Matching
Rust enums are far more powerful than enums in many other languages—they can hold data and represent complex states. Idiomatic code uses enums to model distinct possibilities and pattern matching to handle them cleanly.
Key Practices:
- Use enums for state with variants: Instead of booleans or magic constants, define enums to represent clear states (e.g.,
Status::Loading,Status::Success,Status::Error). - Prefer
matchoverif-elsechains:matchensures exhaustiveness (the compiler checks all variants are handled), preventing bugs. - Use
if letfor single-variant checks: When you only care about one variant,if letis more concise thanmatch.
Example: Enum for State Management
// Non-idiomatic: Using booleans/magic numbers
fn handle_response(success: bool, code: i32) {
if success {
println!("Success! Code: {}", code);
} else {
if code == 404 {
println!("Not found");
} else if code == 500 {
println!("Server error");
} else {
println!("Unknown error: {}", code);
}
}
}
// Idiomatic: Using an enum with data
enum Response {
Success(i32), // Holds a status code
Error(ErrorKind),
}
enum ErrorKind {
NotFound,
ServerError,
Unknown(i32),
}
fn handle_response_idiomatic(response: Response) {
match response {
Response::Success(code) => println!("Success! Code: {}", code),
Response::Error(ErrorKind::NotFound) => println!("Not found"),
Response::Error(ErrorKind::ServerError) => println!("Server error"),
Response::Error(ErrorKind::Unknown(code)) => println!("Unknown error: {}", code),
}
}
// Usage
let success = Response::Success(200);
let error = Response::Error(ErrorKind::NotFound);
handle_response_idiomatic(success); // "Success! Code: 200"
handle_response_idiomatic(error); // "Not found"
Example: if let for Single Variants
let maybe_number: Option<i32> = Some(42);
// Instead of a full match for a single case:
match maybe_number {
Some(n) => println!("Found a number: {}", n),
_ => (), // Ignore other cases
}
// Use if let for conciseness:
if let Some(n) = maybe_number {
println!("Found a number: {}", n);
}
3. Handle Errors Gracefully with Result and Option
Rust encourages explicit error handling. Idiomatic code avoids unwrap() in production (it panics!) and instead uses Result for recoverable errors and Option for missing values.
Key Practices:
- Return
Result<T, E>for fallible functions: Let callers decide how to handle errors (e.g., retry, log, exit). - Use
?to propagate errors: The?operator short-circuits and returns errors, making error handling concise. - Define custom error types with
thiserror: For clean, descriptive errors (avoidsBox<dyn Error>in public APIs). - Prefer
OptionoverResult<T, ()>: UseOption<T>when the “error” is simply “value missing.”
Example: From unwrap() to Result
// Non-idiomatic: Using unwrap() (panics on error!)
fn read_file(path: &str) -> String {
std::fs::read_to_string(path).unwrap() // Panics if file not found!
}
// Idiomatic: Return Result and use ?
use std::fs;
use std::io;
fn read_file_idiomatic(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path) // Returns Result<String, io::Error>
}
// Caller handles the error
match read_file_idiomatic("data.txt") {
Ok(content) => println!("Content: {}", content),
Err(e) => eprintln!("Failed to read file: {}", e), // Graceful error handling
}
Example: Custom Error Type with thiserror
Add thiserror to Cargo.toml:
[dependencies]
thiserror = "1.0"
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Parse error: {0}")]
ParseError(#[from] std::num::ParseIntError),
}
fn parse_file(path: &str) -> Result<i32, MyError> {
let content = fs::read_to_string(path)
.map_err(|_| MyError::FileNotFound(path.to_string()))?;
let number = content.trim().parse()?; // Converts ParseIntError to MyError::ParseError
Ok(number)
}
// Usage
match parse_file("number.txt") {
Ok(n) => println!("Parsed number: {}", n),
Err(e) => eprintln!("Error: {}", e), // "File not found: number.txt" or "Parse error: ..."
}
4. Prefer Iterators Over Loops
Rust’s iterators are lazy, composable, and often more readable than manual loops. They leverage Rust’s type system to ensure correctness and can be optimized to match handwritten loops.
Key Practices:
- Chain iterator methods: Use
map,filter,fold, andcollectto transform data declaratively. - Avoid
for i in 0..nloops: Prefer iterating over collections directly (for item in &collection). - Use
collect()with type annotations: Explicitly specify the target type (e.g.,collect::<Vec<_>>()) for clarity.
Example: Summing Even Numbers
// Non-idiomatic: Manual loop
let numbers = vec![1, 2, 3, 4, 5];
let mut sum = 0;
for i in 0..numbers.len() {
let num = numbers[i];
if num % 2 == 0 {
sum += num;
}
}
// Idiomatic: Iterator chain
let sum: i32 = numbers.iter()
.filter(|&&num| num % 2 == 0) // Keep even numbers
.sum(); // Sum the filtered values
assert_eq!(sum, 6); // 2 + 4 = 6
Example: Transforming Data with Iterators
let words = vec!["hello", "world", "rust"];
// Uppercase and collect into Vec<String>
let uppercased: Vec<String> = words.iter()
.map(|s| s.to_uppercase())
.collect();
assert_eq!(uppercased, vec!["HELLO", "WORLD", "RUST"]);
5. Organize Code with Modules and Crates
Idiomatic Rust code is organized logically into modules and crates, with clear public APIs. This makes code reusable and easy to navigate.
Key Practices:
- Split code into modules by responsibility: e.g.,
parser,network,utils. - Use
pubselectively: Only expose what’s needed for the public API; keep implementation details private. - Re-export with
pub use: Simplify APIs by re-exporting items from submodules (e.g.,pub use parser::parse;instead of forcing users to importmycrate::parser::parse).
Example: Module Structure
// src/lib.rs
pub mod math {
// Public function
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Private helper (not exposed)
fn validate(a: i32) -> bool {
a >= 0
}
pub mod advanced {
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
}
}
// Re-export advanced::multiply for simpler API
pub use math::advanced::multiply;
// Usage (in another file)
use mycrate::math::add;
use mycrate::multiply; // Re-exported, no need for mycrate::math::advanced::multiply
assert_eq!(add(2, 3), 5);
assert_eq!(multiply(2, 3), 6);
6. Follow Naming Conventions
Rust has strict naming conventions, and idiomatic code adheres to them for consistency.
| Item Type | Convention | Example |
|---|---|---|
| Functions | snake_case | calculate_average |
| Variables | snake_case | user_input |
| Types (enums, structs, traits) | CamelCase | UserAccount, Shape |
| Constants | UPPER_SNAKE_CASE | MAX_RETRIES |
| Modules/Crates | snake_case | my_crate, file_utils |
| Lifetimes | 'a, 'b (single lowercase letters) | fn foo<'a>(x: &'a str) |
Additional Tips:
- Avoid abbreviations unless universally understood (e.g.,
io,fmt). - Name booleans with “is_”, “has_”, or “should_” (e.g.,
is_valid,has_permission).
7. Leverage the Standard Library
Rust’s standard library (std) is robust and optimized. Idiomatic code prefers std types over custom implementations.
Key Types to Use:
String/&str: For text (avoid C-style*const u8).Vec<T>: For dynamic arrays (resizable, efficient).HashMap<K, V>/BTreeMap<K, V>: For key-value storage (useBTreeMapfor ordered keys).Option<T>/Result<T, E>: For optional values and error handling (see Section 3).
Example: Using HashMap for Counting
use std::collections::HashMap;
fn count_words(words: &[&str]) -> HashMap<&str, usize> {
let mut counts = HashMap::new();
for &word in words {
*counts.entry(word).or_insert(0) += 1;
}
counts
}
let words = vec!["hello", "world", "hello"];
let counts = count_words(&words);
assert_eq!(counts.get("hello"), Some(&2));
assert_eq!(counts.get("world"), Some(&1));
8. Document and Communicate Clearly
Idiomatic Rust code is self-documenting and provides helpful feedback.
Key Practices:
- Document with
///comments: Explain what functions do, their parameters, return values, and examples (use/// # Examplesfor code snippets). - Derive
Debugfor all types:#[derive(Debug)]lets you print values with{:?}, aiding debugging. - Use
expectfor actionable panic messages: When panicking is intentional (e.g., in tests),expect("reason")is clearer thanunwrap().
Example: Documented Function
/// Adds two numbers.
///
/// # Arguments
/// * `a` - The first number to add.
/// * `b` - The second number to add.
///
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
9. Optimize Without Sacrificing Safety
Rust is performant by default, but idiomatic code avoids premature optimization. When optimizing:
- Prefer stack allocations: Use
Vec::with_capacity(n)if you know the size upfront (avoids reallocations). - Use
&stroverStringfor read-only text (avoids heap allocation). - Profile first: Use
cargo benchorperfto identify bottlenecks before optimizing.
Example: Vec::with_capacity
// Without capacity (may reallocate multiple times)
let mut v = Vec::new();
for i in 0..1000 {
v.push(i);
}
// With capacity (single allocation)
let mut v = Vec::with_capacity(1000); // Pre-allocates space for 1000 elements
for i in 0..1000 {
v.push(i);
}
Conclusion
Writing idiomatic Rust is about more than style—it’s about leveraging Rust’s strengths (ownership, enums, iterators) to write safe, readable, and maintainable code. By following these practices—embracing ownership, using enums and pattern matching, handling errors gracefully, and leaning on the standard library—you’ll write code that feels “Rusty” and integrates seamlessly with the ecosystem.
Remember: Tools like rustfmt (auto-formats code) and clippy (lints for idiomatic issues) are your allies. Run cargo fmt to format code and cargo clippy to catch non-idiomatic patterns!