Introduction: Why Clean Code in Rust Is Not Just a Style
Rust is a language that combines the performance of C++ with memory safety, but its unique ownership and borrowing system requires a special approach to writing code. The Clean Code principles formulated by Robert Martin take on a new dimension in Rust: here, code cleanliness is directly linked to correctness, performance, and maintainability.
In this article, we will explore how to apply Rust idioms to create self-documenting, safe, and efficient code. You will see how to replace typical beginner mistakes with elegant solutions accepted in the Rust community.
1. Idiomatic Error Handling: From unwrap() to Expressive Patterns
One of the first problems beginners face is the overuse of unwrap(). In Clean Code, an error is not an exception, but a part of the architecture. Rust provides powerful Option and Result types for explicit handling of edge cases.
Bad Example (Dirty Code)
fn parse_number(input: &str) -> i32 { input.parse::<i32>().unwrap()}
fn main() { let val = parse_number("not a number"); println!("{}", val); // Panic!}Good Example (Clean Code)
use std::num::ParseIntError;
fn parse_number(input: &str) -> Result<i32, ParseIntError> { input.parse::<i32>()}
fn main() { match parse_number("42") { Ok(n) => println!("Number: {}", n), Err(e) => eprintln!("Error: {}", e), }}Why is this Clean Code? The function explicitly states that it can fail. The calling code decides how to handle the failure — log it, retry, or propagate the error upwards.
Advanced Technique: The ? Operator
fn read_config(path: &str) -> Result<String, std::io::Error> { let content = std::fs::read_to_string(path)?; Ok(content)}The ? operator is an elegant replacement for verbose match expressions. It returns the error upwards if the result is Err, and extracts the value if it is Ok. This reduces nesting and improves readability.
2. Ownership and Borrowing: Design the Architecture, Don't Fight the Compiler
Rust's ownership system may seem like a limitation, but it actually helps write clean code. Problems start when a developer tries to "trick" the compiler with clone() or Rc<RefCell> without necessity.
Anti-pattern: Excessive Cloning
fn process_data(data: Vec<String>) { for item in data.clone() { // Why clone? println!("{}", item); } // data is no longer used}Clean Solution: Borrowing by Reference
fn process_data(data: &[String]) { for item in data { println!("{}", item); }}
fn main() { let my_data = vec!["a".to_string(), "b".to_string()]; process_data(&my_data); // Ownership stays with main println!("Vector is still accessible: {:?}", my_data);}Clean Code Principle: Minimal scope for mutations. If a function should not modify data — accept an immutable reference &T. If modification is needed — &mut T. This makes the code predictable and safe.
Idiom: Passing Ownership for Transformation
fn to_uppercase(mut text: String) -> String { text.make_ascii_uppercase(); text // Ownership is returned}Here, the function "consumes" the string, modifies it, and returns it back. This is efficient (no copying) and explicit: the caller understands that the original variable is no longer valid.