Rust 100 Exercises in Readable Code 100 Techniques For Writing Readable Rust Code (Kanro Tomoya)
Rust 100 Exercises in Readable Code 100 Techniques For Writing Readable Rust Code (Kanro Tomoya)
Use pattern matching with match for clear and concise control flow.
Leverage Option and Result types for safe and explicit error handling.
Utilize iter, map, and filter for functional-style data processing.
Employ enum to represent a type that can be one of several variants.
Use trait to define shared behavior across different types.
Name variables and functions descriptively to convey their purpose.
Use snake_case for variable and function names for consistency.
Prefix boolean variables with is_, has_, or can_ to indicate their nature.
Use const and static for constants to make their immutability clear.
Name modules and files to reflect their contents and purpose.
Avoid abbreviations that are not universally understood.
Use full words instead of single letters for variable names.
Avoid using similar names for different variables to prevent confusion.
Ensure function names clearly describe their actions or results.
Use consistent naming conventions throughout the codebase.
Use impl blocks to group related methods for a type.
Leverage From and Into traits for type conversions.
Use Cow (Clone on Write) for efficient handling of borrowed and owned
data.
Utilize Rc and Arc for reference counting and shared ownership.
Use RefCell and Mutex for interior mutability and thread safety.
Comment on the purpose of complex algorithms or data structures.
Explain the reasoning behind non-obvious design decisions.
Document the expected input and output of functions.
Comment on the usage of unsafe code and its safety guarantees.
Use doc comments (///) to generate documentation for public APIs.
Keep comments up-to-date with code changes.
Avoid redundant comments that restate the obvious.
Use comments to explain why, not what, the code is doing.
Be concise and to the point in your comments.
Use TODO comments to indicate areas for future improvement.
Use ? operator for concise error propagation.
Leverage async and await for asynchronous programming.
Use Box for heap allocation and dynamic dispatch.
Utilize Vec for dynamic arrays and HashMap for key-value storage.
Use slice and str for efficient string and array handling.
Format code consistently using rustfmt.
Organize code into modules and submodules logically.
Use whitespace and indentation to enhance readability.
Group related code together and separate different sections with blank lines.
Avoid deeply nested code by refactoring into smaller functions.
Use if let and while let for concise conditional checks.
Prefer for loops over while loops for iteration.
Use break and continue judiciously to control loop flow.
Avoid complex nested match statements by refactoring into functions.
Use return early to handle error cases and reduce nesting.
Use match guards to add conditions to pattern matches.
Leverage Result's combinators like map and and_then.
Use unwrap_or and unwrap_or_else for default values.
Utilize Option's combinators like map and and_then.
Use Result and Option for error handling and optional values.
Leverage Cow for efficient handling of borrowed and owned data.
Use Rc and Arc for reference counting and shared ownership.
Utilize RefCell and Mutex for interior mutability and thread safety.
Use Box for heap allocation and dynamic dispatch.
Leverage async and await for asynchronous programming.
Use descriptive names for variables to indicate their purpose.
Avoid Single-Letter Variable Names
Use 'let' for Declarations, 'mut' Only When Necessary
Group Related Variables
Use Constants for Unchanging Values
Break down complex functions into smaller, single-purpose functions.
Use helper functions to encapsulate repetitive code.
Avoid side effects in functions
Use clear and descriptive function names
Limit the number of parameters a function takes
Use impl blocks to group related methods for a type
Leverage From and Into traits for type conversions
Use Cow (Clone on Write) for efficient handling of borrowed and owned
data
Utilize Rc and Arc for reference counting and shared ownership
Use RefCell and Mutex for interior mutability and thread safety
Break Down Large Expressions
Use Intermediate Variables
Refactor complex expressions into helper functions
Use parentheses to make the order of operations explicit
Avoid chaining too many method calls in a single line.
Identify and extract unrelated sub-problems into separate functions.
Use helper functions to encapsulate distinct tasks.
Modularize code to separate concerns and improve readability.
Use traits to define shared behavior and reduce code duplication.
Refactor large functions into smaller, more manageable pieces.
Use ? operator for concise error propagation.
Leverage async and await for asynchronous programming.
Use Box for heap allocation and dynamic dispatch.
Utilize Vec for dynamic arrays and HashMap for key-value storage.
Use slice and str for efficient string and array handling.
Choose Vec for dynamic arrays when the size is unknown at compile time.
Use HashMap for key-value pairs when fast lookup is needed.
Leverage BTreeMap for ordered key-value storage.
Use Option and Result for optional and error-prone values.
Choose Rc and Arc for shared ownership and reference counting.
Use Result for Error Handling
Leverage unwrap_or and unwrap_or_else for Default Values
Use expect with meaningful error messages for debugging.
Handle errors at the appropriate level of abstraction.
Use thiserror or anyhow crates for custom error types and handling.
Write generic functions to handle multiple types.
Use traits to define shared behavior and enable polymorphism
Leverage impl Trait for concise and flexible function signatures
Use macro_rules! to create reusable macros
Refactor common patterns into reusable functions or modules
Use crate and pub keywords to control visibility and reuse.
Leverage Cargo workspaces to manage multiple related packages.
Introduction
◆
Welcome to this eBook, which aims to guide you through 100 techniques
for writing more readable code in Rust.
In this book, we delve into the unique features and idiomatic practices of
Rust to help you craft code that is not only functional but also easy for
others to read and understand.
Using match in Rust allows you to handle various cases explicitly, making
the code more readable and maintainable.
< Good Code >
enum Direction {
North,
South,
East,
West,
}
fn get_direction_name(direction: Direction) -> &'static str {
match direction {
Direction::North => "North",
Direction::South => "South",
Direction::East => "East",
Direction::West => "West",
}
}
fn main() {
let direction = Direction::North;
println!("Direction: {}", get_direction_name(direction));
}
<Bad Code>
enum Direction {
North,
South,
East,
West,
}
fn get_direction_name(direction: Direction) -> &'static str {
if let Direction::North = direction {
"North"
} else if let Direction::South = direction {
"South"
} else if let Direction::East = direction {
"East"
} else if let Direction::West = direction {
"West"
} else {
"Unknown"
}
}
fn main() {
let direction = Direction::North;
println!("Direction: {}", get_direction_name(direction));
}
The good example uses match to handle all possible cases of the Direction
enum in a clear and concise manner. The bad example uses multiple if let
statements, which are less readable and more error-prone.
<Memo>
Pattern matching in Rust is exhaustive, meaning the compiler ensures all
possible cases are handled, which helps prevent bugs.
2
Leverage Option and Result types for
safe and explicit error handling.
Using Option and Result types in Rust ensures safe and explicit error
handling.
Rust's Option and Result types provide a way to handle potential errors and
absent values explicitly, improving code safety and readability.
< Good Code >
<Bad Code>
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
panic!("Cannot divide by zero");
} else {
a/b
}
}
fn main() {
println!("Result: {}", divide(4.0, 2.0));
println!("Result: {}", divide(4.0, 0.0)); // This will cause a panic
}
The good example uses the Result type to handle the division operation
safely, providing clear error messages without causing a panic. The bad
example uses panic!, which abruptly stops the program and is less user-
friendly.
<Memo>
Rust's Option and Result types are inspired by similar constructs in
functional programming languages like Haskell and Scala, promoting safer
and more predictable error handling.
3
Utilize iter, map, and filter for functional-
style data processing.
Use Rust's iterators, map, and filter to write clean, readable, and efficient
data processing code.
Using iterators, map, and filter in Rust allows for a functional programming
style that can make your code more concise and expressive. This approach
helps in processing collections in a readable and efficient manner.
< Good Code >
<Bad Code>
The good example uses Rust's iterator methods to process the data in a
functional style, making the code more concise and expressive. The bad
example uses a traditional for loop, which is more verbose and less
readable. The functional approach is generally preferred in Rust for its
clarity and efficiency.
<Memo>
Rust's iterator methods are inspired by functional programming languages
like Haskell and Scala, which emphasize immutability and higher-order
functions.
4
Employ enum to represent a type that can
be one of several variants.
Use Rust's enum to define types that can have multiple variants, making
your code more expressive and type-safe.
Enums in Rust allow you to define a type that can be one of several
variants. This is useful for representing data that can take on different forms
and for ensuring type safety in your code.
< Good Code >
<Bad Code>
The good example uses an enum to represent different shapes, making the
code more type-safe and expressive. The bad example uses constants and
arrays, which are less readable and more error-prone. Enums provide a clear
and structured way to handle multiple variants of a type.
<Memo>
Rust's enums are algebraic data types, similar to those found in functional
programming languages like Haskell. They allow for powerful pattern
matching and type-safe handling of different data variants.
5
Use trait to define shared behavior across
different types.
Traits in Rust allow you to define shared behavior across different types,
promoting code reusability and readability.
Using traits in Rust helps to encapsulate shared behavior, making the code
more modular and easier to understand. This example demonstrates how to
use traits effectively.
< Good Code >
<Bad Code>
<Memo>
Traits in Rust are similar to interfaces in other programming languages like
Java and C#. They allow you to define a set of methods that a type must
implement, promoting polymorphism and code reuse.
6
Name variables and functions
descriptively to convey their purpose.
Using descriptive names for variables and functions makes the code more
readable and easier to understand.
<Bad Code>
<Memo>
Descriptive naming is a fundamental principle in clean code practices. It
helps in maintaining and scaling the codebase, especially in collaborative
environments where multiple developers work on the same project.
7
Use snake_case for variable and function
names for consistency.
Using snake_case for variable and function names ensures consistency and
readability in Rust code.
<Bad Code>
In the good example, the function name calculate_area and variable names
rectangle_width and rectangle_height follow the snake_case convention,
making the code consistent and easy to read. In the bad example, the
function name calculateArea and variable names rectangleWidth and
rectangleHeight use camelCase, which is inconsistent with Rust's naming
conventions and can make the code harder to read and maintain.
<Memo>
Rust's naming conventions are inspired by other languages like Python,
which also uses snake_case for variable and function names. This
consistency helps developers transition between languages more smoothly.
8
Prefix boolean variables with is_, has_,
or can_ to indicate their nature.
Prefixing boolean variables with is_, has_, or can_ makes their purpose
clear and improves code readability.
Using prefixes like is_, has_, or can_ for boolean variables provides
immediate context about their purpose, making the code more intuitive and
easier to understand. This practice helps other developers quickly grasp the
logic and intent behind the code.
< Good Code >
<Bad Code>
// Bad example without boolean prefixes
fn even(number: i32) -> bool {
number % 2 == 0
}
fn permission(user_role: &str) -> bool {
user_role == "admin"
}
fn drive(age: u32) -> bool {
age >= 18
}
let number = 4;
let user_role = "admin";
let age = 20;
println!("Is the number even? {}", even(number));
println!("Does the user have permission? {}", permission(user_role));
println!("Can the person drive? {}", drive(age));
<Memo>
Using descriptive prefixes for boolean variables is a common practice in
many programming languages, not just Rust. This convention helps
improve code clarity and maintainability across different codebases and
teams.
9
Use const and static for constants to
make their immutability clear.
Using const and static in Rust ensures that constants are immutable and
clearly defined.
In Rust, using const and static for constants helps in making the code more
readable and maintainable. It clearly indicates that these values are
immutable and should not be changed, which can prevent bugs and improve
code clarity.
< Good Code >
<Bad Code>
<Memo>
In Rust, const is used for compile-time constants, while static is used for
global constants that have a fixed address in memory. Both ensure
immutability, but static can also be used for mutable global variables with
the static mut keyword, though this is generally discouraged.
10
Name modules and files to reflect their
contents and purpose.
Naming modules and files appropriately helps in understanding the code
structure and purpose.
In Rust, naming modules and files to reflect their contents and purpose is
crucial for code readability and maintainability. It allows other developers
to quickly understand the role of each module and file, making the codebase
easier to navigate and work with.
< Good Code >
// File: user.rs
pub mod user {
pub struct User {
pub id: u32,
pub name: String,
}
impl User {
pub fn new(id: u32, name: String) -> User {
User { id, name }
}
}
}
// File: main.rs
mod user;
fn main() {
let user = user::User::new(1, String::from("Alice"));
println!("User: {} - {}", user.id, user.name);
}
<Bad Code>
// File: module1.rs
pub mod module1 {
pub struct Data {
pub id: u32,
pub name: String,
}
impl Data {
pub fn new(id: u32, name: String) -> Data {
Data { id, name }
}
}
}
// File: main.rs
mod module1;
fn main() {
let data = module1::Data::new(1, String::from("Alice"));
println!("Data: {} - {}", data.id, data.name);
}
In the good example, the file and module are named user, which clearly
indicates that it deals with user-related functionality. This makes it easy to
understand the purpose of the module. In the bad example, the file and
module are named module1, which is vague and does not provide any
information about its contents or purpose, making the code harder to
understand and maintain.
<Memo>
Rust's module system allows for hierarchical organization of code. By using
meaningful names for modules and files, developers can create a clear and
logical structure, which is especially beneficial in large projects.
11
Avoid abbreviations that are not
universally understood.
Use clear and universally understood terms instead of abbreviations to
enhance code readability.
When writing Rust code, using abbreviations that are not widely recognized
can make the code difficult to understand for others. It's important to use
clear and descriptive names to ensure that anyone reading the code can
easily grasp its purpose.
< Good Code >
<Bad Code>
In the good example, the function and variable names are clear and
descriptive, making it easy to understand what the code does. In the bad
example, abbreviations like calc_tp, ip, and tr are used, which are not
immediately clear to someone reading the code for the first time.
<Memo>
Using clear and descriptive names not only helps others understand your
code but also aids in maintaining and debugging the code in the future.
12
Use full words instead of single letters
for variable names.
Avoid using single letters for variable names; instead, use full words to
make the code more readable.
Single-letter variable names can be ambiguous and make the code harder to
read and understand. Using full words for variable names provides context
and clarity, making the code more maintainable.
< Good Code >
<Bad Code>
In the good example, the variable names length, width, and area are used,
which clearly describe their purpose. In the bad example, single letters l, w,
and a are used, which do not provide enough context and can be confusing.
<Memo>
Using full words for variable names can also help with code searchability,
making it easier to find specific variables and understand their usage
throughout the codebase.
13
Avoid using similar names for different
variables to prevent confusion.
Using distinct and descriptive variable names helps prevent confusion and
makes the code more readable.
When variables have similar names, it can be easy to mix them up, leading
to bugs and making the code harder to understand. Clear and distinct names
improve readability and maintainability.
< Good Code >
<Bad Code>
<Memo>
In Rust, using the let keyword allows you to create variables with clear and
descriptive names. This practice is essential for writing maintainable and
bug-free code.
14
Ensure function names clearly describe
their actions or results.
Function names should be descriptive and indicate what the function does
or what result it produces.
Clear and descriptive function names help other developers understand the
purpose and behavior of the function without needing to read its
implementation. This practice enhances code readability and
maintainability.
< Good Code >
<Bad Code>
<Memo>
In Rust, function names are typically written in snake_case, which means
words are separated by underscores. This convention helps improve
readability and consistency in the codebase.
15
Use consistent naming conventions
throughout the codebase.
Adopt a uniform naming convention to enhance code readability and
maintainability.
<Bad Code>
<Memo>
Rust's naming conventions are inspired by other languages like Python and
Ruby, which also use snake_case for variable and function names. This
consistency helps developers who are familiar with these languages to adapt
more quickly to Rust.
16
Use impl blocks to group related
methods for a type.
Group related methods within impl blocks to organize code logically and
improve readability.
Using impl blocks to group methods related to a specific type makes the
code more modular and easier to understand. It also helps in maintaining a
clear structure, especially as the number of methods grows.
< Good Code >
<Bad Code>
In the good example, all methods related to the Rectangle struct are grouped
within an impl block, making it clear that these methods belong to the
Rectangle type. This organization improves readability and maintainability.
In the bad example, the functions are defined separately from the Rectangle
struct, making it harder to associate them with the type and reducing the
modularity of the code.
<Memo>
The use of impl blocks in Rust is similar to the use of classes in object-
oriented languages like C++ and Java, where methods are defined within
the class. This approach helps in encapsulating behavior and data together,
promoting better software design principles.
17
Leverage From and Into traits for type
conversions.
Using the From and Into traits in Rust to simplify and standardize type
conversions.
The From and Into traits in Rust provide a standardized way to convert
between types. Implementing these traits can make your code more
readable and maintainable by clearly defining how types should be
converted.
< Good Code >
In the good example, the From trait is implemented for MyType, allowing
for a clear and standardized conversion from i32 to MyType. This makes
the code more readable and maintainable. The Into trait is automatically
implemented for types that implement From, allowing for easy conversion
back to the original type. In the bad example, manual conversions are used,
which can lead to more verbose and error-prone code.
<Memo>
The From trait is often preferred over the Into trait when implementing
conversions because From is more flexible and can be automatically
converted into Into. This means that if you implement From<T> for U, you
get Into<U> for T for free.
18
Use Cow (Clone on Write) for efficient
handling of borrowed and owned data.
Utilize the Cow (Clone on Write) type in Rust to efficiently manage data
that can be either borrowed or owned.
The Cow (Clone on Write) type in Rust allows you to work with data that
can be either borrowed or owned, providing flexibility and efficiency. It is
particularly useful when you need to perform operations that may require
ownership of the data but want to avoid unnecessary cloning.
< Good Code >
use std::borrow::Cow;
fn process_data(input: &str) -> Cow<str> {
if input.contains("special") {
// If the input contains "special", we need to own the data to modify it
let mut owned_data = input.to_string();
owned_data.push_str(" processed");
Cow::Owned(owned_data)
} else {
// Otherwise, we can just borrow the input data
Cow::Borrowed(input)
}
}
fn main() {
let borrowed_data = "This is a test";
let owned_data = "This is a special test";
let result1 = process_data(borrowed_data);
let result2 = process_data(owned_data);
println!("Result 1: {}", result1);
println!("Result 2: {}", result2);
}
<Bad Code>
In the good example, the Cow type is used to handle both borrowed and
owned data efficiently. When the input data does not need to be modified, it
is borrowed, avoiding unnecessary cloning. When modification is required,
the data is cloned and owned. In the bad example, the input data is always
cloned, even when it is not necessary, leading to inefficient memory usage.
<Memo>
The Cow type is part of the standard library and stands for "Clone on
Write." It is particularly useful in scenarios where you want to avoid
unnecessary allocations and copies, such as when working with large
datasets or performance-critical applications.
19
Utilize Rc and Arc for reference counting
and shared ownership.
Use Rc and Arc to manage shared ownership of data in Rust, ensuring
memory safety and preventing data races.
use std::rc::Rc;
use std::sync::Arc;
use std::thread;
// Single-threaded example with Rc
fn main() {
let data = Rc::new(vec![1, 2, 3]);
let data_clone = Rc::clone(&data);
println!("Data: {:?}", data_clone);
}
// Multi-threaded example with Arc
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("Data from thread: {:?}", data_clone);
});
handle.join().unwrap();
}
<Bad Code>
use std::rc::Rc;
use std::sync::Arc;
use std::thread;
// Incorrect usage of Rc in a multi-threaded context
fn main() {
let data = Rc::new(vec![1, 2, 3]);
let data_clone = Rc::clone(&data);
let handle = thread::spawn(move || {
// This will cause a compile-time error
println!("Data from thread: {:?}", data_clone);
});
handle.join().unwrap();
}
// Unnecessary use of Arc in a single-threaded context
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);
println!("Data: {:?}", data_clone);
}
<Memo>
Rc stands for Reference Counted, and Arc stands for Atomic Reference
Counted. The atomic operations in Arc make it safe for use across threads,
but they come with a performance cost compared to Rc.
20
Use RefCell and Mutex for interior
mutability and thread safety.
RefCell and Mutex allow for interior mutability and thread-safe access to
data in Rust, enabling safe mutation of data even when it is shared.
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
use std::thread;
// Single-threaded example with RefCell
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
{
let mut data_borrow = data.borrow_mut();
data_borrow.push(4);
}
println!("Data: {:?}", data.borrow());
}
// Multi-threaded example with Mutex
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut data_lock = data_clone.lock().unwrap();
data_lock.push(4);
println!("Data from thread: {:?}", *data_lock);
});
handle.join().unwrap();
println!("Data: {:?}", *data.lock().unwrap());
}
<Bad Code>
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
use std::thread;
// Incorrect usage of RefCell in a multi-threaded context
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
let data_clone = data.clone();
let handle = thread::spawn(move || {
// This will cause a runtime panic
let mut data_borrow = data_clone.borrow_mut();
data_borrow.push(4);
});
handle.join().unwrap();
}
// Unnecessary use of Mutex in a single-threaded context
fn main() {
let data = Mutex::new(vec![1, 2, 3]);
{
let mut data_lock = data.lock().unwrap();
data_lock.push(4);
}
println!("Data: {:?}", *data.lock().unwrap());
}
<Memo>
RefCell uses Rust's borrow checker at runtime, allowing for mutable
borrows even when the data is behind an immutable reference. Mutex
provides mutual exclusion, ensuring that only one thread can access the
data at a time, preventing data races.
21
Comment on the purpose of complex
algorithms or data structures.
Explain the purpose and benefits of using complex algorithms or data
structures in Rust.
<Bad Code>
// This function uses a simple vector to store data, which can be inefficient
for large datasets.
fn main() {
let mut vec = Vec::new();
vec.push((1, "one"));
vec.push((2, "two"));
vec.push((3, "three"));
// Inefficiently retrieve values
for (key, value) in &vec {
if *key == 2 {
println!("The value for key 2 is: {}", value);
}
}
}
The good example uses a B-tree, which is a balanced tree data structure that
provides efficient insertions, deletions, and lookups. This is particularly
useful for large datasets where performance is critical. The bad example
uses a simple vector, which can become inefficient as the dataset grows
because it requires linear search to find elements.
<Memo>
B-trees are widely used in databases and file systems due to their efficiency
in handling large amounts of data.
22
Explain the reasoning behind non-
obvious design decisions.
Discuss the rationale behind design decisions that may not be immediately
clear to other developers.
<Bad Code>
// This function uses integers to represent different states of a process.
// Using integers makes the code less readable and more error-prone.
fn main() {
let state = 2;
match state {
0 => println!("Process has started."),
1 => println!("Process is in progress."),
2 => println!("Process is completed."),
3 => println!("Process has failed."),
_ => println!("Unknown state."),
}
}
<Memo>
Enums in Rust are powerful and can also hold data, making them versatile
for various use cases beyond simple state representation.
23
Document the expected input and output
of functions.
Clearly document the expected input parameters and the output of your
functions.
<Bad Code>
fn add(a: i32, b: i32) -> i32 {
a+b
}
fn main() {
let result = add(5, 3);
println!("The sum is: {}", result); // The sum is: 8
}
<Memo>
Rust's documentation comments use triple slashes (///) and support
Markdown, allowing for rich formatting and easy generation of HTML
documentation using tools like rustdoc.
24
Comment on the usage of unsafe code
and its safety guarantees.
Clearly comment on why and how unsafe code is used, and what safety
guarantees are being upheld.
<Memo>
Rust's unsafe keyword allows for operations that the compiler cannot
guarantee to be safe, such as dereferencing raw pointers or calling foreign
functions. However, it is the programmer's responsibility to ensure that
these operations do not violate Rust's safety guarantees.
25
Use doc comments (///) to generate
documentation for public APIs.
Doc comments (///) in Rust are used to generate documentation for public
APIs, making it easier for others to understand and use your code.
<Bad Code>
<Memo>
Rust's documentation system, rustdoc, automatically generates HTML
documentation from doc comments, making it easy to create professional-
looking documentation for your projects.
26
Keep comments up-to-date with code
changes.
Ensure that comments are always updated to reflect the current state of the
code to avoid confusion and maintain code readability.
<Bad Code>
In the good example, the comment accurately describes the function and its
behavior. In the bad example, the code was changed from multiplication to
addition, but the comment was not updated, leading to potential confusion
and errors.
<Memo>
Outdated comments can be more misleading than no comments at all.
Always review and update comments when making changes to the code to
ensure they remain accurate and helpful.
27
Avoid redundant comments that restate
the obvious.
Write comments that add value and avoid stating what the code already
clearly expresses.
<Bad Code>
<Memo>
The term "factorial" comes from the Latin word "factor," meaning "maker."
The factorial function is widely used in permutations, combinations, and
other mathematical computations.
28
Use comments to explain why, not what,
the code is doing.
Comments should clarify the reasoning behind the code, not just describe
its functionality.
Effective comments explain the rationale behind decisions, making the code
more understandable and maintainable.
< Good Code >
<Bad Code>
<Memo>
HashMaps in Rust are implemented using a hashing algorithm called
SipHash, which is designed to be fast and secure, making it suitable for
general-purpose use.
29
Be concise and to the point in your
comments.
Write comments that are brief yet informative, focusing on the "why" rather
than the "what."
Clear and concise comments help other developers understand the purpose
and logic of your code without overwhelming them with unnecessary
details.
< Good Code >
<Bad Code>
In the good example, the comment is brief and focuses on the purpose of
the function. In the bad example, the comment is overly detailed and
explains what the code does, which is already clear from the code itself.
<Memo>
Excessive comments can clutter your code and make it harder to read. Aim
for comments that add value by explaining the reasoning behind complex
logic or decisions.
30
Use TODO comments to indicate areas
for future improvement.
Mark sections of code that need enhancement or revisiting with TODO
comments.
<Bad Code>
In the good example, the TODO comment clearly indicates an area for
future improvement. In the bad example, the comments are redundant and
do not provide actionable information for future development.
<Memo>
TODO comments can be easily searched and tracked in most code editors,
making it simple to revisit and address them later.
31
Use ? operator for concise error
propagation.
The ? operator simplifies error handling by propagating errors
automatically.
Using the ? operator in Rust allows for more readable and concise error
handling by reducing boilerplate code.
< Good Code >
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // If File::open returns an error, it is
propagated
let mut content = String::new();
file.read_to_string(&mut content)?; // If read_to_string returns an error, it
is propagated
Ok(content) // If no errors, return the content
}
<Bad Code>
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(path: &str) -> Result<String, io::Error> {
let mut file = match File::open(path) {
Ok(file) => file,
Err(e) => return Err(e), // Manually handling the error
};
let mut content = String::new();
match file.read_to_string(&mut content) {
Ok(_) => Ok(content),
Err(e) => return Err(e), // Manually handling the error
}
}
<Memo>
The ? operator was introduced in Rust 1.13 and is syntactic sugar for the
try! macro, which was used for error propagation before its introduction.
32
Leverage async and await for
asynchronous programming.
Using async and await in Rust allows for writing asynchronous code that is
both efficient and easy to read.
use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};
async fn read_file_content(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path).await?; // Asynchronously open the file
let mut content = String::new();
file.read_to_string(&mut content).await?; // Asynchronously read the file
content
Ok(content) // Return the content if no errors
}
// Main function to run the async function
#[tokio::main]
async fn main() {
match read_file_content("example.txt").await {
Ok(content) => println!("File content: {}", content),
Err(e) => eprintln!("Error reading file: {}", e),
}
}
<Bad Code>
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // Synchronously open the file
let mut content = String::new();
file.read_to_string(&mut content)?; // Synchronously read the file content
Ok(content) // Return the content if no errors
}
// Main function to run the sync function
fn main() {
match read_file_content("example.txt") {
Ok(content) => println!("File content: {}", content),
Err(e) => eprintln!("Error reading file: {}", e),
}
}
In the good example, async and await are used to handle file operations
asynchronously, which can improve performance by not blocking the
thread. The bad example uses synchronous file operations, which can block
the thread and reduce performance, especially in I/O-bound applications.
<Memo>
Rust's async/await syntax was stabilized in version 1.39, making it easier to
write asynchronous code without relying on external libraries like futures or
combinators.
33
Use Box for heap allocation and dynamic
dispatch.
Box is used in Rust for heap allocation and dynamic dispatch, making code
more readable and efficient.
Using Box in Rust allows for heap allocation, which is useful for large data
structures or when the size of the data is not known at compile time. It also
enables dynamic dispatch, which is essential for polymorphism.
< Good Code >
<Bad Code>
In the good example, Box<dyn Shape> is used to allocate the Circle on the
heap and enable dynamic dispatch. This is useful when the exact type of the
shape is not known at compile time. In the bad example, a reference to a
trait object is used, which does not involve heap allocation and may not be
suitable for all use cases.
<Memo>
The Box type in Rust is a smart pointer that provides ownership for heap-
allocated data. It is often used for recursive data structures and dynamic
dispatch.
34
Utilize Vec for dynamic arrays and
HashMap for key-value storage.
Vec and HashMap are essential for managing dynamic arrays and key-value
pairs in Rust, enhancing code readability and efficiency.
Vec is a growable array type in Rust, while HashMap is used for storing
key-value pairs. Both are fundamental for handling dynamic data structures
and are widely used in Rust programming.
< Good Code >
use std::collections::HashMap;
fn main() {
// Using Vec for a dynamic array
let mut numbers: Vec<i32> = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
println!("Numbers: {:?}", numbers);
// Using HashMap for key-value storage
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.insert(String::from("Bob"), 20);
println!("Scores: {:?}", scores);
}
<Bad Code>
fn main() {
// Using a fixed-size array instead of Vec
let numbers: [i32; 3] = [1, 2, 3];
println!("Numbers: {:?}", numbers);
// Using a vector of tuples instead of HashMap
let scores: Vec<(&str, i32)> = vec![("Alice", 10), ("Bob", 20)];
println!("Scores: {:?}", scores);
}
In the good example, Vec is used for a dynamic array, allowing for flexible
and efficient data management. HashMap is used for key-value storage,
providing quick lookups and insertions. In the bad example, a fixed-size
array and a vector of tuples are used, which are less flexible and efficient for
dynamic data management.
<Memo>
Vec and HashMap are part of Rust's standard library and are highly
optimized for performance. They are commonly used in Rust programs for
their flexibility and efficiency in handling dynamic data.
35
Use slice and str for efficient string and
array handling.
Using slices and str in Rust allows for efficient and safe handling of strings
and arrays.
Slices and str are powerful tools in Rust that enable efficient manipulation
of strings and arrays without unnecessary copying. They provide a view
into a sequence of elements, making operations faster and more memory-
efficient.
< Good Code >
fn main() {
let s = String::from("hello world");
// Using a slice to get a part of the string
let hello = &s[0..5];
let world = &s[6..11];
println!("{} {}", hello, world); // Output: hello world
let arr = [1, 2, 3, 4, 5];
// Using a slice to get a part of the array
let slice = &arr[1..4];
for elem in slice {
println!("{}", elem); // Output: 2 3 4
}
}
<Bad Code>
fn main() {
let s = String::from("hello world");
// Creating new strings instead of using slices
let hello = s[0..5].to_string();
let world = s[6..11].to_string();
println!("{} {}", hello, world); // Output: hello world
let arr = [1, 2, 3, 4, 5];
// Creating a new vector instead of using a slice
let slice = vec![arr[1], arr[2], arr[3]];
for elem in slice {
println!("{}", elem); // Output: 2 3 4
}
}
In the good example, slices are used to reference parts of the string and
array without creating new allocations, making the code more efficient. In
the bad example, new strings and vectors are created, which is less efficient
and unnecessary.
<Memo>
Slices in Rust are always references, which means they do not own the data
they point to. This makes them lightweight and efficient for read-only
operations.
36
Format code consistently using rustfmt.
Using rustfmt ensures that your Rust code is consistently formatted, making
it easier to read and maintain.
<Bad Code>
<Memo>
fmt can be integrated into your development workflow by adding it to your
editor or running it as part of your build process, ensuring that all code is
consistently formatted before being committed.
37
Organize code into modules and
submodules logically.
Structuring your Rust code into logical modules and submodules enhances
readability and maintainability.
// src/main.rs
mod math_operations {
pub mod arithmetic {
pub fn add(a: i32, b: i32) -> i32 {
a+b
}
pub fn subtract(a: i32, b: i32) -> i32 {
a-b
}
}
pub mod algebra {
pub fn solve_quadratic(a: f64, b: f64, c: f64) -> (f64, f64) {
let discriminant = b * b - 4.0 * a * c;
let root1 = (-b + discriminant.sqrt()) / (2.0 * a);
let root2 = (-b - discriminant.sqrt()) / (2.0 * a);
(root1, root2)
}
}
}
fn main() {
let sum = math_operations::arithmetic::add(5, 3);
let roots = math_operations::algebra::solve_quadratic(1.0, -3.0, 2.0);
println!("Sum: {}", sum);
println!("Roots: {:?}", roots);
}
<Bad Code>
// src/main.rs
fn add(a: i32, b: i32) -> i32 {
a+b
}
fn subtract(a: i32, b: i32) -> i32 {
a-b
}
fn solve_quadratic(a: f64, b: f64, c: f64) -> (f64, f64) {
let discriminant = b * b - 4.0 * a * c;
let root1 = (-b + discriminant.sqrt()) / (2.0 * a);
let root2 = (-b - discriminant.sqrt()) / (2.0 * a);
(root1, root2)
}
fn main() {
let sum = add(5, 3);
let roots = solve_quadratic(1.0, -3.0, 2.0);
println!("Sum: {}", sum);
println!("Roots: {:?}", roots);
}
<Memo>
Rust's module system allows you to create a hierarchy of modules, making
it easier to manage large codebases by logically grouping related
functionalities.
38
Use whitespace and indentation to
enhance readability.
Proper use of whitespace and consistent indentation improves the
readability and structure of your Rust code.
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
for number in &numbers {
if number % 2 == 0 {
println!("{} is even", number);
} else {
println!("{} is odd", number);
}
}
}
<Bad Code>
fn main(){
let numbers=vec![1,2,3,4,5];
for number in &numbers{
if number%2==0{
println!("{} is even",number);
}else{
println!("{} is odd",number);
}
}
}
In the good example, proper indentation and spacing are used to clearly
separate different parts of the code, making it easy to read and understand.
In the bad example, the lack of whitespace and inconsistent indentation
make the code difficult to follow.
<Memo>
Rust's rustfmt tool can automatically format your code according to
standard style guidelines, ensuring consistent use of whitespace and
indentation.
39
Group related code together and separate
different sections with blank lines.
Organize your code by grouping related sections together and using blank
lines to separate different sections.
Grouping related code together and separating different sections with blank
lines improves readability and helps others understand the structure of your
code.
< Good Code >
<Bad Code>
In the good example, related functions are grouped together, and blank lines
separate different sections, making the code easier to read. In the bad
example, everything is crammed together, making it hard to distinguish
between different parts of the code.
<Memo>
Using blank lines to separate code sections is a common practice in many
programming languages, not just Rust. It helps in maintaining a clean and
readable codebase.
40
Avoid deeply nested code by refactoring
into smaller functions.
Refactor deeply nested code into smaller, more manageable functions to
improve readability and maintainability.
<Bad Code>
fn main() {
for num in 1..10 {
if num % 2 == 0 {
println!("{} is even", num);
}
}
}
In the good example, the logic for checking if a number is even is separated
into its own function, making the main function cleaner and more readable.
In the bad example, the logic is nested within the main function, making it
harder to follow.
<Memo>
Refactoring code into smaller functions not only improves readability but
also makes it easier to test individual components, leading to more robust
and maintainable code.
41
Use if let and while let for concise
conditional checks.
Simplify conditional checks using if let and while let for more readable and
maintainable code.
Using if let and while let in Rust allows for more concise and readable
conditional checks, especially when dealing with Option and Result types.
< Good Code >
<Bad Code>
// Bad example using traditional `if` and `while`
fn main() {
let some_option = Some(5);
// Using traditional `if` which is more verbose
if some_option.is_some() {
let value = some_option.unwrap();
println!("The value is: {}", value);
} else {
println!("No value found");
}
}
// Bad example using traditional `while`
fn process_numbers(numbers: &mut Vec<i32>) {
// Using traditional `while` which is more verbose
while numbers.len() > 0 {
let number = numbers.pop().unwrap();
println!("Processing number: {}", number);
}
}
The good examples use if let and while let to make the code more concise
and readable. if let allows for pattern matching in a single line, reducing
verbosity. Similarly, while let simplifies loops that process elements until a
condition is met. The bad examples use traditional if and while constructs,
which are more verbose and less readable.
<Memo>
if let and while let are part of Rust's pattern matching capabilities, which are
inspired by functional programming languages like Haskell and ML.
42
Prefer for loops over while loops for
iteration.
Use for loops instead of while loops for iterating over collections to
improve code readability and safety.
In Rust, for loops are preferred for iterating over collections because they
are more concise, safer, and less error-prone compared to while loops.
< Good Code >
<Bad Code>
The good example uses a for loop, which is more concise and eliminates the
need for manual index management. This reduces the risk of off-by-one
errors and makes the code easier to read. The bad example uses a while
loop, which requires manual index management and is more verbose,
increasing the potential for errors.
<Memo>
Rust's for loops are based on iterators, which provide a powerful and
flexible way to process sequences of elements. The iterator pattern is a
common feature in many modern programming languages, including
Python and C++.
43
Use break and continue judiciously to
control loop flow.
Using break and continue wisely can make your loops more readable and
maintainable.
In Rust, controlling the flow of loops with break and continue can help
avoid deeply nested conditions and make the code easier to follow.
< Good Code >
<Bad Code>
In the good example, break and continue are used to control the loop flow
directly, making the code more readable and straightforward. The bad
example includes unnecessary else branches that clutter the code and make
it harder to read.
<Memo>
The break statement exits the loop immediately, while the continue
statement skips the rest of the current loop iteration and proceeds to the next
iteration.
44
Avoid complex nested match statements
by refactoring into functions.
Refactor complex nested match statements into separate functions to
improve readability and maintainability.
In Rust, deeply nested match statements can make the code difficult to read
and understand. Refactoring these into separate functions can simplify the
logic and enhance code clarity.
< Good Code >
enum Command {
Start,
Stop,
Pause,
Resume,
}
fn handle_start() {
println!("Handling start command");
}
fn handle_stop() {
println!("Handling stop command");
}
fn handle_pause() {
println!("Handling pause command");
}
fn handle_resume() {
println!("Handling resume command");
}
fn execute_command(command: Command) {
match command {
Command::Start => handle_start(),
Command::Stop => handle_stop(),
Command::Pause => handle_pause(),
Command::Resume => handle_resume(),
}
}
fn main() {
let command = Command::Start;
execute_command(command);
}
<Bad Code>
enum Command {
Start,
Stop,
Pause,
Resume,
}
fn main() {
let command = Command::Start;
match command {
Command::Start => {
println!("Handling start command");
}
Command::Stop => {
println!("Handling stop command");
}
Command::Pause => {
println!("Handling pause command");
}
Command::Resume => {
println!("Handling resume command");
}
}
}
In the good example, the logic for handling each command is refactored
into separate functions, making the match statement in execute_command
clean and easy to read. The bad example has all the logic directly within the
match arms, leading to a cluttered and less maintainable code structure.
<Memo>
Rust's match statement is exhaustive, meaning it requires all possible cases
to be handled, which helps prevent runtime errors due to unhandled cases.
45
Use return early to handle error cases and
reduce nesting.
Returning early in functions helps handle errors efficiently and keeps the
code less nested and more readable.
Handling errors early in Rust functions can prevent deep nesting and make
the code easier to follow. This technique involves checking for error
conditions at the beginning of the function and returning immediately if an
error is found.
< Good Code >
<Bad Code>
In the good example, error conditions are checked early, and the function
returns immediately if an error is found. This reduces nesting and makes the
code easier to read. In the bad example, the code is deeply nested, making it
harder to follow and maintain.
<Memo>
The early return pattern is common in many programming languages and is
often referred to as "guard clauses" or "early exits." It helps improve code
readability and maintainability.
46
Use match guards to add conditions to
pattern matches.
Match guards in Rust allow you to add additional conditions to pattern
matches, making your code more expressive and precise.
<Bad Code>
In the good example, match guards are used to add conditions directly
within the match arms, making the code concise and easy to read. In the bad
example, additional nested if statements are used within the match arm,
making the code more complex and harder to follow.
<Memo>
Match guards in Rust are a powerful feature that can be used to handle
complex matching scenarios. They are particularly useful when you need to
match on a pattern and also check additional conditions.
47
Leverage Result's combinators like map
and and_then.
Using combinators like map and and_then can make your Rust code more
readable and concise.
In Rust, the Result type is used for error handling. Combinators like map
and and_then allow you to transform and chain results in a clean and
readable way.
< Good Code >
The good example uses map and and_then combinators to handle the Result
type more concisely. This reduces boilerplate code and makes the logic
clearer. The bad example uses match statements, which are more verbose
and can make the code harder to read.
<Memo>
The map combinator is used to transform the Ok value of a Result, while
and_then is used to chain multiple operations that return Result types.
48
Use unwrap_or and unwrap_or_else for
default values.
Using unwrap_or and unwrap_or_else provides a clean way to handle
default values for Option and Result types.
<Memo>
unwrap_or takes a default value directly, while unwrap_or_else takes a
closure that provides the default value, which can be useful for more
complex default value computations.
49
Utilize Option's combinators like map
and and_then.
Using combinators like map and and_then with Option can make your Rust
code more readable and concise.
In Rust, the Option type is used to represent a value that can be either Some
(a value) or None (no value). Combinators like map and and_then allow
you to transform and chain operations on Option values in a clean and
readable way.
< Good Code >
<Bad Code>
<Memo>
The Option type in Rust is inspired by similar constructs in functional
programming languages like Haskell and ML, where it is known as Maybe.
50
Use Result and Option for error handling
and optional values.
Leveraging Result and Option types for error handling and representing
optional values can make your Rust code more robust and expressive.
In Rust, Result is used for functions that can return an error, while Option is
used for values that may or may not be present. Using these types
effectively can help you write safer and more predictable code.
< Good Code >
<Bad Code>
The good example properly handles Result and Option types using match
statements to account for both success and failure cases. This makes the
code more robust and prevents runtime panics. The bad example uses
unwrap, which can cause the program to panic if the Result is an Err or the
Option is None. Proper error handling is crucial for writing reliable and
maintainable Rust code.
<Memo>
The Result type in Rust is similar to the Either type in functional
programming languages, where it represents a value that can be one of two
possible types, typically used for error handling.
51
Leverage Cow for efficient handling of
borrowed and owned data.
Using Cow (Clone on Write) allows for efficient handling of data that can
be either borrowed or owned, reducing unnecessary cloning.
The Cow enum in Rust is useful when you need to work with data that
might be either borrowed or owned. It helps in optimizing performance by
avoiding unnecessary cloning until it's absolutely necessary.
< Good Code >
use std::borrow::Cow;
fn process_data(input: &str) -> Cow<str> {
if input.contains("special") {
// If the input contains "special", we need to modify it, so we clone it.
Cow::Owned(input.replace("special", "ordinary"))
} else {
// Otherwise, we can just borrow the input.
Cow::Borrowed(input)
}
}
fn main() {
let data = "This is a special string.";
let result = process_data(data);
println!("{}", result); // Output: This is a ordinary string.
}
<Bad Code>
In the good example, Cow is used to avoid unnecessary cloning. If the input
does not need modification, it is borrowed, saving memory and processing
time. In the bad example, the input is always cloned, which is inefficient.
<Memo>
Cow stands for "Clone on Write" and is part of the std::borrow module. It is
particularly useful in scenarios where you want to avoid unnecessary
allocations and copies.
52
Use Rc and Arc for reference counting
and shared ownership.
Rc and Arc are smart pointers that enable multiple ownership of data
through reference counting, with Arc being thread-safe.
use std::rc::Rc;
use std::sync::Arc;
use std::thread;
fn main() {
// Single-threaded example with Rc
let data = Rc::new("Hello, Rc!".to_string());
let data_clone = Rc::clone(&data);
println!("{}", data_clone); // Output: Hello, Rc!
// Multi-threaded example with Arc
let data = Arc::new("Hello, Arc!".to_string());
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("{}", data_clone); // Output: Hello, Arc!
});
handle.join().unwrap();
}
<Bad Code>
use std::rc::Rc;
use std::sync::Arc;
use std::thread;
fn main() {
// Single-threaded example with Rc
let data = Rc::new("Hello, Rc!".to_string());
let data_clone = data.clone(); // Incorrectly using clone instead of
Rc::clone
println!("{}", data_clone); // Output: Hello, Rc!
// Multi-threaded example with Arc
let data = Arc::new("Hello, Arc!".to_string());
let data_clone = data.clone(); // Incorrectly using clone instead of
Arc::clone
let handle = thread::spawn(move || {
println!("{}", data_clone); // Output: Hello, Arc!
});
handle.join().unwrap();
}
In the good example, Rc::clone and Arc::clone are used to increment the
reference count properly. In the bad example, the clone method is used
incorrectly, which can lead to confusion and potential errors.
<Memo>
Rc and Arc are part of Rust's standard library. Rc is used for single-threaded
reference counting, while Arc is used for thread-safe reference counting,
making it suitable for concurrent programming.
53
Utilize RefCell and Mutex for interior
mutability and thread safety.
RefCell and Mutex are used in Rust to allow interior mutability and ensure
thread safety, respectively.
<Bad Code>
<Memo>
RefCell and Mutex are part of Rust's standard library and are essential tools
for managing interior mutability and thread safety. RefCell is used for
single-threaded scenarios, while Mutex is designed for multi-threaded
environments.
54
Use Box for heap allocation and dynamic
dispatch.
Box is used in Rust for heap allocation and enabling dynamic dispatch
through trait objects.
Box allows for values to be stored on the heap, which is useful for large
data structures or when the size of the data is not known at compile time. It
also enables dynamic dispatch by allowing trait objects.
< Good Code >
// Good example using Box for heap allocation and dynamic dispatch
trait Animal {
fn speak(&self);
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}
impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog),
Box::new(Cat)];
for animal in animals {
animal.speak();
}
}
<Bad Code>
// Bad example using Box for heap allocation and dynamic dispatch
trait Animal {
fn speak(&self);
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}
impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}
fn main() {
let animals: Vec<dyn Animal> = vec![Dog, Cat]; // This will not compile
for animal in animals {
animal.speak();
}
}
In the good example, Box is used to store Dog and Cat on the heap and
enable dynamic dispatch through the Animal trait. This allows for a
collection of different types that implement the same trait. In the bad
example, the code attempts to create a vector of trait objects without using
Box, which is not allowed in Rust as trait objects must be behind a pointer.
<Memo>
Box is one of Rust's smart pointers and is used for heap allocation. It is
particularly useful for recursive data structures and enabling polymorphism
through trait objects.
55
Leverage async and await for
asynchronous programming.
Using async and await in Rust allows for efficient and readable
asynchronous code.
<Bad Code>
use std::thread;
use std::time::Duration;
// A blocking function that simulates a delay
fn perform_task() {
println!("Task started");
thread::sleep(Duration::from_secs(2));
println!("Task completed");
}
fn main() {
println!("Starting main function");
perform_task();
println!("Main function completed");
}
The good example uses async and await to perform a non-blocking delay,
making the code more efficient and readable. The bad example uses a
blocking thread::sleep, which can cause the entire program to wait,
reducing efficiency and readability.
<Memo>
Rust's async/await syntax is inspired by similar features in other languages
like JavaScript and C#, but it is designed to work with Rust's ownership and
concurrency model.
56
Use descriptive names for variables to
indicate their purpose.
Choosing descriptive names for variables helps in understanding the code's
intent and functionality.
<Bad Code>
<Memo>
Descriptive variable names are a key aspect of clean code principles, which
emphasize readability and maintainability. This practice is advocated by
many programming experts, including Robert C. Martin in his book "Clean
Code."
57
Avoid Single-Letter Variable Names
Use descriptive variable names to enhance code readability.
<Bad Code>
The good code example uses descriptive variable and function names,
making it immediately clear what each element represents. The bad code
uses single-letter names, which can be confusing and harder to understand,
especially in larger codebases. While both codes function identically, the
good code is much more readable and maintainable.
<Memo>
The use of descriptive variable names is part of a broader concept called
"self-documenting code," which aims to make code as clear and
understandable as possible without relying heavily on comments.
58
Use 'let' for Declarations, 'mut' Only
When Necessary
Declare variables with 'let' and only use 'mut' for variables that need to be
mutable.
The following examples show how to properly use 'let' for variable
declarations and 'mut' only when necessary in a function that calculates the
sum of numbers in a vector.
< Good Code >
<Bad Code>
The good code example uses 'mut' only for the 'total' variable, which needs
to be mutable to accumulate the sum. It also uses a reference to the input
vector, avoiding unnecessary mutation. The bad code unnecessarily marks
several variables as mutable, including the input vector and the result,
which don't need to change. This can lead to potential errors and makes the
code less clear about which values are expected to change.
<Memo>
Rust's approach to mutability is part of its focus on safety. By defaulting to
immutability, Rust helps prevent accidental modifications and makes it
easier to reason about code behavior.
59
Group Related Variables
Organize related variables together to improve code readability and
maintainability.
<Bad Code>
In the good example, related variables are grouped together within the
struct definition and when initializing the struct. This organization makes it
easier to understand the purpose of each variable and their relationships.
The bad example, on the other hand, mixes unrelated variables, making it
harder to grasp the structure and purpose of the data.
<Memo>
Rust's struct fields are private by default, promoting encapsulation and data
hiding.
60
Use Constants for Unchanging Values
Employ constants for values that remain unchanged throughout the
program's execution.
The following code illustrates the use of constants for improved readability
and maintainability:
< Good Code >
<Memo>
In Rust, constants are always inlined at compile-time, which can lead to
performance improvements in some cases.
61
Break down complex functions into
smaller, single-purpose functions.
Simplify complex functions by dividing them into smaller, more
manageable functions that each serve a single purpose.
In the good example, the code is broken down into two single-purpose
functions: calculate_area and calculate_perimeter. This makes the code
more modular and easier to understand. In the bad example, a single
function calculate_area_and_perimeter does both calculations, making it
harder to read and maintain.
<Memo>
The Single Responsibility Principle (SRP) is one of the SOLID principles
of object-oriented design. It states that a class or function should have only
one reason to change, meaning it should only have one job or responsibility.
62
Use helper functions to encapsulate
repetitive code.
Encapsulate repetitive code into helper functions to reduce redundancy and
improve readability.
<Bad Code>
fn main() {
let names = vec!["Alice", "Bob", "Charlie"];
for name in names {
println!("Hello, {}!", name); // Repetitive code
}
}
In the good example, the repetitive code for printing a greeting message is
encapsulated in the print_greeting helper function. This makes the main
function cleaner and more readable. In the bad example, the greeting
message is printed directly within the loop, leading to code repetition.
<Memo>
Encapsulating repetitive code into helper functions is a common practice in
software development known as DRY (Don't Repeat Yourself). This
principle aims to reduce the repetition of code patterns and improve code
maintainability.
63
Avoid side effects in functions
Write pure functions that don't modify external state
<Bad Code>
The good example uses a pure function that takes an array slice as input and
returns the sum without modifying any external state. It uses the fold
method to iterate through the array and calculate the sum.
The bad example uses a static mutable variable TOTAL to accumulate the
sum, which introduces side effects. This approach is harder to understand,
less predictable, and not thread-safe.
<Memo>
Pure functions are easier to test, reason about, and can be safely used in
parallel computations.
64
Use clear and descriptive function names
Choose function names that accurately describe their purpose
The following examples show how to name a function that calculates the
average of an array:
< Good Code >
<Bad Code>
<Memo>
Clear function names act as documentation, making code self-explanatory
and reducing the need for additional comments.
65
Limit the number of parameters a
function takes
Keep function parameters to a minimum for improved readability and
maintainability.
The good example uses a struct to group related parameters, making the
function signature cleaner and more maintainable. This approach is
especially useful when dealing with functions that require many parameters.
The bad example shows a function with multiple parameters, which can be
harder to read and maintain, especially as the number of parameters grows.
<Memo>
In Rust, when a function takes more than 7 parameters, it's often considered
a code smell and a sign that the function might be doing too much or that
the parameters should be grouped.
66
Use impl blocks to group related
methods for a type
Organize related methods for a type using impl blocks to improve code
structure and readability.
The following code shows how to use impl blocks to group methods for a
struct, improving organization and readability.
< Good Code >
<Bad Code>
The good example uses impl blocks to group all methods related to the
Rectangle struct. This approach improves code organization and makes it
clear which methods belong to the Rectangle type. It also allows for method
chaining and a more object-oriented style. The bad example defines
functions separately, which can lead to scattered code and doesn't take
advantage of Rust's method syntax.
<Memo>
In Rust, you can have multiple impl blocks for the same type, which is
useful for organizing methods into logical groups or separating
implementation details.
67
Leverage From and Into traits for type
conversions
Use From and Into traits to implement clean and idiomatic type conversions
in Rust.
The following code demonstrates how to implement From trait for custom
type conversions:
< Good Code >
struct Person {
name: String,
age: u32,
}
fn create_person(name: String, age: u32) -> Person {
Person { name, age }
}
fn main() {
let tuple = ("Alice".to_string(), 30);
let person = create_person(tuple.0, tuple.1);
println!("Name: {}, Age: {}", person.name, person.age);
}
The good code example implements the From trait for Person, allowing
seamless conversion from a tuple to Person using the .into() method. This
approach is more idiomatic and flexible.
The bad code example uses a separate function to create a Person, which is
less flexible and doesn't leverage Rust's type conversion traits.
<Memo>
Implementing From automatically provides the reciprocal Into
implementation for free.
68
Use Cow (Clone on Write) for efficient
handling of borrowed and owned data
Utilize Cow for flexible and efficient handling of data that can be either
borrowed or owned.
The following code demonstrates the use of Cow to handle both borrowed
and owned strings efficiently:
< Good Code >
use std::borrow::Cow;
fn process_string(input: &str) -> Cow<str> {
if input.contains("rust") {
// Return a borrowed version if no modification is needed
Cow::Borrowed(input)
} else {
// Return an owned version with modifications
Cow::Owned(input.to_uppercase())
}
}
fn main() {
let s1 = "I love rust programming";
let s2 = "I love coding";
println!("Processed: {}", process_string(s1)); // Borrowed
println!("Processed: {}", process_string(s2)); // Owned
}
<Bad Code>
The good code example uses Cow to efficiently handle both borrowed and
owned data. It avoids unnecessary allocations when no modifications are
needed.
The bad code example always returns a String, which results in unnecessary
allocations and copying of data, even when no modifications are made.
<Memo>
Cow stands for "Clone on Write" and is part of the std::borrow module in
Rust's standard library.
69
Utilize Rc and Arc for reference counting
and shared ownership
Rc and Arc provide shared ownership of values, allowing multiple owners
without copying data.
use std::rc::Rc;
// Define a simple struct
struct SharedData {
value: i32,
}
fn main() {
// Create an Rc-wrapped SharedData instance
let shared = Rc::new(SharedData { value: 42 });
// Create multiple references to the same data
let reference1 = Rc::clone(&shared);
let reference2 = Rc::clone(&shared);
// Use the shared data
println!("Value from reference1: {}", reference1.value);
println!("Value from reference2: {}", reference2.value);
// Check the reference count
println!("Reference count: {}", Rc::strong_count(&shared));
}
<Bad Code>
// Incorrect usage: manually managing shared ownership
struct SharedData {
value: i32,
}
fn main() {
let data = SharedData { value: 42 };
let reference1 = &data;
let reference2 = &data;
// This could lead to ownership issues if we try to move 'data'
println!("Value: {}", reference1.value);
println!("Value: {}", reference2.value);
// No easy way to track how many references exist
}
<Memo>
Rc stands for "Reference Counted" and is designed for single-threaded
scenarios. For multi-threaded applications, use Arc (Atomic Reference
Counted) instead.
70
Use RefCell and Mutex for interior
mutability and thread safety
RefCell and Mutex provide interior mutability, allowing mutable borrows
of immutable values in single-threaded and multi-threaded contexts
respectively.
The following code demonstrates the use of RefCell for interior mutability
in a single-threaded context:
< Good Code >
use std::cell::RefCell;
struct Counter {
value: RefCell<i32>,
}
impl Counter {
fn new() -> Self {
Counter {
value: RefCell::new(0),
}
}
fn increment(&self) {
*self.value.borrow_mut() += 1;
}
fn get_value(&self) -> i32 {
*self.value.borrow()
}
}
fn main() {
let counter = Counter::new();
counter.increment();
counter.increment();
println!("Counter value: {}", counter.get_value());
}
<Bad Code>
struct Counter {
value: i32,
}
impl Counter {
fn new() -> Self {
Counter { value: 0 }
}
fn increment(&self) {
// This won't compile: cannot mutate through a shared reference
self.value += 1;
}
fn get_value(&self) -> i32 {
self.value
}
}
fn main() {
let counter = Counter::new();
// These lines would cause compilation errors
// counter.increment();
// counter.increment();
println!("Counter value: {}", counter.get_value());
}
The good example uses RefCell to allow interior mutability. This enables
the Counter struct to have methods that can modify its internal state even
when the Counter instance itself is immutable. The bad example attempts to
modify the value directly through a shared reference, which Rust's
borrowing rules prevent. RefCell moves the borrow checking to runtime,
allowing for more flexible designs while maintaining memory safety.
<Memo>
For thread-safe interior mutability, use Mutex instead of RefCell. Mutex
provides similar functionality but with the added guarantee of thread safety
through locking mechanisms.
71
Break Down Large Expressions
Improve code readability by splitting complex expressions into smaller,
named variables.
<Bad Code>
<Memo>
Breaking down expressions not only improves readability but can also help
with debugging and performance optimization in some cases.
72
Use Intermediate Variables
Enhance code clarity by storing results of sub-expressions in intermediate
variables.
This code example shows how using intermediate variables can improve
readability and maintainability:
< Good Code >
<Bad Code>
<Memo>
Using intermediate variables can sometimes help the compiler optimize the
code better, as it provides clearer hints about the programmer's intentions.
73
Refactor complex expressions into helper
functions
Break down complex logic into smaller, named functions for improved
readability.
<Bad Code>
fn main() {
let price = 100.0;
let quantity = 15;
// Complex inline calculation
let final_price = price * (1.0 - (if quantity > 10 { 0.1 } else { 0.05 }) -
(quantity as f64 * 0.01).min(0.2));
println!("Final price: ${:.2}", final_price);
}
<Memo>
Rust's powerful type system and ownership model make it easier to create
and use helper functions without significant performance overhead.
74
Use parentheses to make the order of
operations explicit
Add parentheses to clarify the intended order of operations in complex
expressions.
The following code shows how parentheses can be used to make the order
of operations clear:
< Good Code >
fn main() {
let a = 5;
let b = 3;
let c = 2;
// Explicit order of operations
let result = (a + b) * (c + 1);
println!("Result: {}", result);
// Another example with more complex expression
let x = 10;
let y = 4;
let z = ((x + y) * 2) - (x / y);
println!("z: {}", z);
}
<Bad Code>
fn main() {
let a = 5;
let b = 3;
let c = 2;
// Ambiguous order of operations
let result = a + b * c + 1;
println!("Result: {}", result);
// Another example with ambiguous expression
let x = 10;
let y = 4;
let z = x + y * 2 - x / y;
println!("z: {}", z);
}
The good code example uses parentheses to explicitly show the intended
order of operations. This makes it immediately clear how the expressions
should be evaluated. The bad code example omits parentheses, relying on
the default operator precedence. While this may produce the correct result,
it requires the reader to recall precedence rules, potentially leading to
misunderstandings.
<Memo>
While Rust follows standard operator precedence rules, using parentheses
not only clarifies intent but can also prevent subtle bugs when modifying
complex expressions later.
75
Avoid chaining too many method calls in
a single line.
Chaining too many method calls in a single line can make the code difficult
to read and debug.
// Good example: Method calls are broken into multiple lines for clarity.
let data = vec![1, 2, 3, 4, 5];
let filtered_data = data.iter()
.filter(|&&x| x > 2)
.map(|&x| x * 2)
.collect::<Vec<_>>();
println!("{:?}", filtered_data); // Output: [6, 8, 10]
<Bad Code>
In the good example, the method calls are broken into multiple lines,
making it easier to read and understand each step of the process. In the bad
example, chaining all the method calls in a single line makes the code
harder to read and debug.
<Memo>
Rust's method chaining is powerful, but overusing it can lead to code that is
difficult to maintain. Breaking down method chains into multiple lines can
significantly improve code readability.
76
Identify and extract unrelated sub-
problems into separate functions.
Extracting unrelated sub-problems into separate functions improves code
modularity and readability.
<Bad Code>
In the good example, the filtering and transformation logic are extracted
into separate functions, making the main function cleaner and the code
more modular. In the bad example, all the logic is crammed into the main
function, making it harder to read and maintain.
<Memo>
Modular code is easier to test and debug. By extracting sub-problems into
separate functions, you can write unit tests for each function individually,
ensuring that each part of your code works correctly.
77
Use helper functions to encapsulate
distinct tasks.
Encapsulate distinct tasks into helper functions to improve code readability
and maintainability.
Using helper functions allows you to break down complex tasks into
smaller, more manageable pieces. This makes your code easier to read,
understand, and maintain.
< Good Code >
<Bad Code>
fn main() {
let width = 5.0;
let height = 10.0;
let area = width * height;
println!("The area of the rectangle is: {}", area);
}
In the good example, the task of calculating the area is encapsulated in the
calculate_area function, making the print_area function and main function
cleaner and more readable. In the bad example, the calculation is done
directly in the main function, making it harder to read and maintain.
<Memo>
Helper functions can also improve testability by allowing you to test
smaller units of functionality independently.
78
Modularize code to separate concerns
and improve readability.
Separate your code into modules to isolate different concerns and enhance
readability.
<Bad Code>
fn main() {
let width = 5.0;
let height = 10.0;
let area = width * height;
println!("The area of the rectangle is: {}", area);
}
In the good example, the code is divided into two modules: geometry for
calculation and printer for printing. This separation of concerns makes the
code more organized and easier to understand. In the bad example, all logic
is crammed into the main function, making it less readable and harder to
maintain.
<Memo>
Rust's module system allows you to group related functions, structs, and
traits together, promoting better code organization and reuse.
79
Use traits to define shared behavior and
reduce code duplication.
Traits in Rust allow you to define shared behavior across different types,
reducing code duplication and enhancing readability.
Using traits to define shared behavior helps in creating a more modular and
maintainable codebase. It allows different types to implement the same
behavior, making the code more flexible and easier to understand.
< Good Code >
<Bad Code>
In the good example, the Describable trait is defined and implemented for
both Dog and Cat structs. This allows the print_description function to work
with any type that implements the Describable trait, reducing code
duplication and making the code more flexible. In the bad example, separate
functions are used for each type, leading to code duplication and less
maintainable code.
<Memo>
Traits in Rust are similar to interfaces in other languages like Java and C#.
They allow you to define a set of methods that a type must implement,
promoting code reuse and polymorphism.
80
Refactor large functions into smaller,
more manageable pieces.
Breaking down large functions into smaller, more manageable pieces
improves readability and maintainability.
<Memo>
The Single Responsibility Principle (SRP) is one of the SOLID principles
of object-oriented design. It states that a function or class should have only
one reason to change, promoting better organization and maintainability of
code.
81
Use ? operator for concise error
propagation.
Simplify error handling in Rust by using the ? operator to propagate errors.
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // If File::open returns an error, it is
propagated
let mut content = String::new();
file.read_to_string(&mut content)?; // If read_to_string returns an error, it
is propagated
Ok(content) // If everything is fine, return the content
}
<Bad Code>
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(path: &str) -> Result<String, io::Error> {
let mut file = match File::open(path) {
Ok(file) => file,
Err(e) => return Err(e), // Manually propagating the error
};
let mut content = String::new();
match file.read_to_string(&mut content) {
Ok(_) => Ok(content),
Err(e) => return Err(e), // Manually propagating the error
}
}
<Memo>
The ? operator can only be used in functions that return a Result or Option.
It was introduced in Rust 1.13 and is syntactic sugar for the try! macro.
82
Leverage async and await for
asynchronous programming.
Use async and await to write asynchronous code that is more readable and
maintainable.
Rust's async and await keywords allow you to write asynchronous code that
looks similar to synchronous code. This makes it easier to read and
understand, while still providing the benefits of asynchronous execution.
< Good Code >
use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};
async fn read_file_content(path: &str) -> io::Result<String> {
let mut file = File::open(path).await?; // Open the file asynchronously
let mut content = String::new();
file.read_to_string(&mut content).await?; // Read the file content
asynchronously
Ok(content) // Return the content if everything is fine
}
#[tokio::main]
async fn main() {
match read_file_content("example.txt").await {
Ok(content) => println!("File content: {}", content),
Err(e) => eprintln!("Error reading file: {}", e),
}
}
<Bad Code>
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(path: &str) -> io::Result<String> {
let mut file = File::open(path)?; // Open the file synchronously
let mut content = String::new();
file.read_to_string(&mut content)?; // Read the file content
synchronously
Ok(content) // Return the content if everything is fine
}
fn main() {
match read_file_content("example.txt") {
Ok(content) => println!("File content: {}", content),
Err(e) => eprintln!("Error reading file: {}", e),
}
}
In the good example, async and await are used to perform file operations
asynchronously. This allows the program to handle other tasks while
waiting for I/O operations to complete, improving efficiency. The bad
example uses synchronous file operations, which can block the thread and
make the program less responsive. Using async and await makes the code
more readable and efficient.
<Memo>
Rust's async/await syntax is inspired by similar features in other languages
like JavaScript and C#. It was stabilized in Rust 1.39 and allows for writing
non-blocking code in a more intuitive way.
83
Use Box for heap allocation and dynamic
dispatch.
Box is used in Rust for heap allocation and dynamic dispatch, allowing for
more flexible and efficient memory management.
Using Box in Rust helps manage heap-allocated data and enables dynamic
dispatch, which is useful for polymorphism. This technique is essential for
writing readable and maintainable code.
< Good Code >
<Bad Code>
In the good example, Box<dyn Shape> is used to allocate the Circle on the
heap and enable dynamic dispatch. This makes the code more flexible and
easier to extend with other shapes. In the bad example, a reference to a trait
object is used, which can lead to lifetime issues and less flexible code.
<Memo>
Box is a smart pointer in Rust that provides ownership for heap-allocated
data. It is often used for recursive data structures and polymorphism.
84
Utilize Vec for dynamic arrays and
HashMap for key-value storage.
Vec and HashMap are essential collections in Rust for managing dynamic
arrays and key-value pairs, respectively.
Using Vec and HashMap in Rust allows for efficient and flexible data
storage and retrieval. These collections are fundamental for writing clear
and maintainable code.
< Good Code >
use std::collections::HashMap;
fn main() {
// Create a dynamic array (Vec)
let mut numbers: Vec<i32> = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
// Iterate over the Vec
for number in &numbers {
println!("Number: {}", number);
}
// Create a HashMap for key-value storage
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.insert(String::from("Bob"), 20);
// Access values in the HashMap
match scores.get("Alice") {
Some(score) => println!("Alice's score: {}", score),
None => println!("Alice's score not found"),
}
}
<Bad Code>
fn main() {
// Create a fixed-size array
let numbers: [i32; 3] = [1, 2, 3];
// Iterate over the array
for number in &numbers {
println!("Number: {}", number);
}
// Create a vector of tuples for key-value storage
let scores: Vec<(&str, i32)> = vec![("Alice", 10), ("Bob", 20)];
// Access values in the vector of tuples
for &(name, score) in &scores {
if name == "Alice" {
println!("Alice's score: {}", score);
}
}
}
In the good example, Vec is used for a dynamic array, allowing for flexible
resizing, and HashMap is used for efficient key-value storage and retrieval.
In the bad example, a fixed-size array and a vector of tuples are used, which
are less flexible and harder to manage.
<Memo>
Vec is a growable array type in Rust, while HashMap is a collection that
associates keys with values. Both are part of Rust's standard library and are
widely used for various data management tasks.
85
Use slice and str for efficient string and
array handling.
Slices and str are efficient ways to handle strings and arrays in Rust,
providing a view into a sequence without copying data.
Using slices and str allows you to work with parts of arrays and strings
efficiently. They provide a way to reference a contiguous sequence of
elements in a collection without owning them, which avoids unnecessary
data copying and improves performance.
< Good Code >
<Bad Code>
In the good example, the function first_word takes a string slice &str as a
parameter, making it more flexible and efficient since it can accept both
String and &str types. The bad example takes a &String, which is less
flexible and unnecessarily restrictive. Using slices avoids unnecessary data
copying and allows for more efficient memory usage.
<Memo>
Slices in Rust are a view into a collection of elements, allowing you to
reference a subset of the collection without owning it. This is particularly
useful for functions that need to operate on parts of strings or arrays without
taking ownership of the data.
86
Choose Vec for dynamic arrays when the
size is unknown at compile time.
Vec is a growable array type in Rust, ideal for situations where the number
of elements is not known at compile time.
When you need a dynamic array whose size can change during runtime, Vec
is the appropriate choice. It provides flexibility and dynamic memory
management, allowing you to add or remove elements as needed.
< Good Code >
fn main() {
// Create a new, empty vector to store integers
let mut numbers: Vec<i32> = Vec::new();
// Add some elements to the vector
numbers.push(1);
numbers.push(2);
numbers.push(3);
// Iterate over the vector and print each element
for number in &numbers {
println!("{}", number);
}
// Remove the last element
numbers.pop();
// Print the modified vector
println!("After pop: {:?}", numbers);
}
<Bad Code>
fn main() {
// Create a fixed-size array
let mut numbers: [i32; 3] = [1, 2, 3];
// Attempt to add an element (this will not compile)
// numbers.push(4);
// Iterate over the array and print each element
for number in &numbers {
println!("{}", number);
}
// Attempt to remove an element (this will not compile)
// numbers.pop();
// Print the array
println!("Array: {:?}", numbers);
}
In the good example, a Vec is used to create a dynamic array that can grow
and shrink as needed. This allows for adding and removing elements at
runtime. The bad example uses a fixed-size array, which does not support
dynamic resizing, making it unsuitable for situations where the number of
elements can change.
<Memo>
Vec in Rust is implemented as a heap-allocated, growable array. It provides
methods like push and pop to add and remove elements, making it highly
versatile for dynamic data storage.
87
Use HashMap for key-value pairs when
fast lookup is needed.
HashMap provides constant time complexity for lookups, making it ideal
for scenarios where fast access to data is crucial.
In Rust, HashMap is a collection that stores key-value pairs and allows for
efficient data retrieval. It is particularly useful when you need to quickly
find a value associated with a specific key.
< Good Code >
use std::collections::HashMap;
fn main() {
// Create a new HashMap
let mut scores = HashMap::new();
// Insert some key-value pairs
scores.insert("Alice", 50);
scores.insert("Bob", 30);
scores.insert("Charlie", 40);
// Retrieve a value using a key
if let Some(score) = scores.get("Alice") {
println!("Alice's score is {}", score);
} else {
println!("Alice's score not found");
}
// Iterate over all key-value pairs
for (name, score) in &scores {
println!("{}: {}", name, score);
}
}
<Bad Code>
use std::collections::HashMap;
fn main() {
// Create a new HashMap
let mut scores = HashMap::new();
// Insert some key-value pairs
scores.insert("Alice", 50);
scores.insert("Bob", 30);
scores.insert("Charlie", 40);
// Retrieve a value using a key without checking if it exists
let score = scores.get("Alice").unwrap();
println!("Alice's score is {}", score);
// Iterate over all key-value pairs without using references
for (name, score) in scores {
println!("{}: {}", name, score);
}
}
In the good example, the code checks if the key exists before attempting to
retrieve the value, preventing potential runtime errors. It also uses
references when iterating over the HashMap to avoid consuming the
collection. In the bad example, the code uses unwrap() without checking if
the key exists, which can cause a panic if the key is not found. Additionally,
it consumes the HashMap during iteration, making it unusable afterward.
<Memo>
HashMap in Rust is implemented using a hashing algorithm, which allows
for average-case constant time complexity for insertions, deletions, and
lookups. This makes it highly efficient for scenarios where performance is
critical.
88
Leverage BTreeMap for ordered key-
value storage.
BTreeMap maintains the order of keys, making it suitable for scenarios
where you need to iterate over keys in a sorted manner.
use std::collections::BTreeMap;
fn main() {
// Create a new BTreeMap
let mut scores = BTreeMap::new();
// Insert some key-value pairs
scores.insert("Alice", 50);
scores.insert("Bob", 30);
scores.insert("Charlie", 40);
// Retrieve a value using a key
if let Some(score) = scores.get("Alice") {
println!("Alice's score is {}", score);
} else {
println!("Alice's score not found");
}
// Iterate over all key-value pairs in sorted order
for (name, score) in &scores {
println!("{}: {}", name, score);
}
}
<Bad Code>
use std::collections::BTreeMap;
fn main() {
// Create a new BTreeMap
let mut scores = BTreeMap::new();
// Insert some key-value pairs
scores.insert("Alice", 50);
scores.insert("Bob", 30);
scores.insert("Charlie", 40);
// Retrieve a value using a key without checking if it exists
let score = scores.get("Alice").unwrap();
println!("Alice's score is {}", score);
// Iterate over all key-value pairs without using references
for (name, score) in scores {
println!("{}: {}", name, score);
}
}
In the good example, the code checks if the key exists before attempting to
retrieve the value, preventing potential runtime errors. It also uses
references when iterating over the BTreeMap to avoid consuming the
collection. In the bad example, the code uses unwrap() without checking if
the key exists, which can cause a panic if the key is not found. Additionally,
it consumes the BTreeMap during iteration, making it unusable afterward.
<Memo>
BTreeMap in Rust is implemented using a B-tree, which keeps the keys in
sorted order and allows for efficient range queries and ordered iteration.
This makes it ideal for applications where the order of elements is
important.
89
Use Option and Result for optional and
error-prone values.
Using Option and Result types in Rust helps handle optional and error-
prone values safely and clearly.
In Rust, Option and Result are powerful enums that allow you to handle
cases where a value might be absent or an operation might fail. This makes
your code more robust and easier to understand.
< Good Code >
<Bad Code>
In the good example, the divide function returns a Result type, which
clearly indicates whether the operation was successful or if an error
occurred. This makes the code safer and easier to read. In the bad example,
the function does not handle the case where the divisor is zero, leading to a
potential runtime error.
<Memo>
The Option type is used when a value might be None, while the Result type
is used for operations that can succeed (Ok) or fail (Err). These types are
part of Rust's standard library and are widely used for error handling and
optional values.
90
Choose Rc and Arc for shared ownership
and reference counting.
Use Rc and Arc for managing shared ownership of values with reference
counting in Rust.
use std::rc::Rc;
use std::sync::Arc;
use std::thread;
// Single-threaded example using Rc
fn single_threaded_example() {
let value = Rc::new(5);
let value_clone = Rc::clone(&value);
println!("Value: {}", value);
println!("Cloned Value: {}", value_clone);
}
// Multi-threaded example using Arc
fn multi_threaded_example() {
let value = Arc::new(5);
let value_clone = Arc::clone(&value);
let handle = thread::spawn(move || {
println!("Value in thread: {}", value_clone);
});
println!("Value in main: {}", value);
handle.join().unwrap();
}
fn main() {
single_threaded_example();
multi_threaded_example();
}
<Bad Code>
use std::rc::Rc;
use std::sync::Arc;
use std::thread;
// Incorrect usage of Rc in a multi-threaded context
fn incorrect_multi_threaded_example() {
let value = Rc::new(5);
let value_clone = Rc::clone(&value);
let handle = thread::spawn(move || {
// This will cause a compile-time error
println!("Value in thread: {}", value_clone);
});
println!("Value in main: {}", value);
handle.join().unwrap();
}
fn main() {
incorrect_multi_threaded_example();
}
<Memo>
Rc stands for Reference Counted and is used for single-threaded scenarios.
Arc stands for Atomic Reference Counted and is used for multi-threaded
scenarios. Both are part of Rust's standard library and help manage shared
ownership of values.
91
Use Result for Error Handling
Utilize Result<T, E> for functions that may fail, providing detailed error
types for better error handling and code clarity.
The following code demonstrates the use of Result for a function that reads
a file:
< Good Code >
use std::fs::File;
use std::io::{self, Read};
// Define a custom error type
#[derive(Debug)]
enum FileReadError {
IoError(io::Error),
EmptyFile,
}
// Function that returns a Result
fn read_file_contents(path: &str) -> Result<String, FileReadError> {
// Open the file, map IoError to our custom error type
let mut file = File::open(path).map_err(FileReadError::IoError)?;
let mut contents = String::new();
// Read file contents, map IoError to our custom error type
file.read_to_string(&mut contents).map_err(FileReadError::IoError)?;
// Check if file is empty and return custom error if so
if contents.is_empty() {
return Err(FileReadError::EmptyFile);
}
Ok(contents)
}
// Usage
fn main() {
match read_file_contents("example.txt") {
Ok(contents) => println!("File contents: {}", contents),
Err(FileReadError::IoError(e)) => eprintln!("IO error: {}", e),
Err(FileReadError::EmptyFile) => eprintln!("The file is empty"),
}
}
<Bad Code>
use std::fs::File;
use std::io::Read;
// Function that panics on error
fn read_file_contents(path: &str) -> String {
let mut file = File::open(path).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
contents
}
// Usage
fn main() {
let contents = read_file_contents("example.txt");
println!("File contents: {}", contents);
}
<Memo>
The ? operator in Rust is syntactic sugar for the try! macro, which helps
propagate errors up the call stack when working with Result types.
92
Leverage unwrap_or and unwrap_or_else
for Default Values
Use unwrap_or and unwrap_or_else methods to provide default values
when working with Option or Result types, improving code readability and
error handling.
use std::env;
fn get_config_value(key: &str) -> String {
// Use unwrap_or to provide a default value
env::var(key).unwrap_or_else(|_| {
println!("Warning: {} not set, using default value", key);
String::from("default_value")
})
}
fn parse_timeout(timeout_str: &str) -> u64 {
// Use unwrap_or to provide a default value if parsing fails
timeout_str.parse().unwrap_or_else(|_| {
println!("Warning: Invalid timeout value, using default");
30 // Default timeout in seconds
})
}
fn main() {
let api_key = get_config_value("API_KEY");
println!("Using API key: {}", api_key);
let timeout = parse_timeout(&get_config_value("TIMEOUT"));
println!("Timeout set to {} seconds", timeout);
}
<Bad Code>
use std::env;
fn get_config_value(key: &str) -> String {
match env::var(key) {
Ok(value) => value,
Err(_) => {
panic!("Configuration value {} not set!", key)
}
}
}
fn parse_timeout(timeout_str: &str) -> u64 {
timeout_str.parse().unwrap()
}
fn main() {
let api_key = get_config_value("API_KEY");
println!("Using API key: {}", api_key);
let timeout = parse_timeout(&get_config_value("TIMEOUT"));
println!("Timeout set to {} seconds", timeout);
}
<Memo>
The unwrap_or method is used when you have a simple default value, while
unwrap_or_else is used when you need to compute the default value lazily
or perform additional operations.
93
Use expect with meaningful error
messages for debugging.
Using expect with clear error messages helps in identifying the cause of a
panic during debugging.
When using Rust, the expect method can be used to handle Option and
Result types. Providing a meaningful error message in expect can
significantly aid in debugging by clearly indicating what went wrong.
< Good Code >
<Bad Code>
In the good example, the expect method provides a clear and specific error
message: "Failed to parse 'num_str' as i32". This message helps the
developer understand exactly what went wrong if the parsing fails. In the
bad example, the error message "Error" is too vague and does not provide
useful information for debugging.
<Memo>
The expect method is often used in Rust for quick prototyping and
debugging. However, in production code, it is generally better to handle
errors more gracefully using methods like unwrap_or_else or proper error
handling with Result.
94
Handle errors at the appropriate level of
abstraction.
Errors should be handled at the level where they can be meaningfully
addressed, ensuring that the code remains clean and maintainable.
In Rust, error handling should be done at the level where the error can be
appropriately managed. This means propagating errors up the call stack
until they can be handled in a context that makes sense, rather than handling
them too early or too late.
< Good Code >
<Bad Code>
// Bad example: Handling errors at the wrong level
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(path: &str) -> String {
let mut file = File::open(path).expect("Failed to open file");
let mut content = String::new();
file.read_to_string(&mut content).expect("Failed to read file");
content
}
fn main() {
let content = read_file_content("example.txt");
println!("File content: {}", content);
}
<Memo>
Rust's error handling is designed to be explicit and robust. The ? operator is
a syntactic sugar that simplifies error propagation, making the code cleaner
and easier to read.
95
Use thiserror or anyhow crates for
custom error types and handling.
Using thiserror or anyhow crates simplifies error handling and makes your
Rust code more readable and maintainable.
In Rust, error handling can be verbose and complex. The thiserror crate
allows you to define custom error types easily, while the anyhow crate
provides a convenient way to handle errors without boilerplate code. Here’s
how to use them effectively.
< Good Code >
<Bad Code>
In the good example, thiserror is used to define a custom error type, making
the error messages more descriptive and the code more maintainable. The
anyhow crate is used to handle errors in a more ergonomic way, providing
context to errors. In the bad example, a generic String is used for error
handling, which is less descriptive and harder to manage.
<Memo>
The thiserror crate is often used in combination with anyhow to provide a
robust error handling strategy in Rust applications. This combination
leverages Rust's powerful type system to create clear and maintainable error
handling code.
96
Write generic functions to handle
multiple types.
Writing generic functions allows your code to handle multiple types,
making it more flexible and reusable.
Generics in Rust enable you to write functions that can operate on different
data types while maintaining type safety. This reduces code duplication and
enhances code readability. Here’s how to write and use generic functions
effectively.
< Good Code >
<Bad Code>
// A non-generic function to find the largest element in a list of integers
fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
// Another non-generic function to find the largest element in a list of chars
fn largest_char(list: &[char]) -> &char {
let mut largest = &list[0];
for item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let char_list = vec!['y', 'm', 'a', 'q'];
println!("The largest number is {}", largest_i32(&number_list));
println!("The largest char is {}", largest_char(&char_list));
}
<Memo>
Rust's generics are zero-cost abstractions, meaning they do not introduce
runtime overhead. The compiler generates specific implementations for
each type used with a generic function, ensuring optimal performance.
97
Use traits to define shared behavior and
enable polymorphism
Traits in Rust allow defining shared behavior across different types,
promoting code reuse and enabling polymorphism.
<Bad Code>
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
fn circle_area(c: &Circle) -> f64 {
std::f64::consts::PI * c.radius * c.radius
}
fn rectangle_area(r: &Rectangle) -> f64 {
r.width * r.height
}
fn main() {
let circle = Circle { radius: 5.0 };
let rectangle = Rectangle { width: 4.0, height: 3.0 };
println!("Circle area: {}", circle_area(&circle));
println!("Rectangle area: {}", rectangle_area(&rectangle));
}
The good code example uses a trait Shape to define a common interface for
different shapes. This allows for polymorphism, as demonstrated by the
print_area function that can work with any type implementing the Shape
trait. The bad code example, on the other hand, uses separate functions for
each shape, leading to code duplication and less flexibility.
<Memo>
Traits in Rust are similar to interfaces in other languages but with the added
benefit of allowing default method implementations.
98
Leverage impl Trait for concise and
flexible function signatures
Using impl Trait in function signatures provides a concise way to work with
traits without explicit generics, improving readability and flexibility.
use std::fmt::Display;
// Function that returns an impl Trait
fn create_greeter() -> impl Fn(String) -> String {
|name| format!("Hello, {}!", name)
}
// Function that takes an impl Trait as an argument
fn print_twice(value: impl Display) {
println!("{}", value);
println!("{}", value);
}
fn main() {
let greeter = create_greeter();
println!("{}", greeter("Alice".to_string()));
print_twice(42);
print_twice("Rust");
}
<Bad Code>
use std::fmt::Display;
fn create_greeter() -> Box<dyn Fn(String) -> String> {
Box::new(|name| format!("Hello, {}!", name))
}
fn print_twice<T: Display>(value: T) {
println!("{}", value);
println!("{}", value);
}
fn main() {
let greeter = create_greeter();
println!("{}", greeter("Alice".to_string()));
print_twice(42);
print_twice("Rust");
}
The good code example uses impl Trait in function signatures, making the
code more concise and easier to read. For create_greeter, it avoids the need
for explicit boxing of the closure. For print_twice, it simplifies the function
signature while still allowing for generic usage. The bad code example uses
explicit generics and trait objects, which can be more verbose and
potentially less performant.
<Memo>
impl Trait was introduced in Rust 1.26 and has since become a powerful
tool for writing expressive and efficient code.
99
Use macro_rules! to create reusable
macros
Macros in Rust allow for code reuse and abstraction, enhancing readability
and maintainability.
The good code example uses a macro to create a reusable logging function.
This approach reduces code duplication and improves maintainability. The
macro takes a severity level and a format string with arguments, making it
flexible and easy to use. The bad code example, on the other hand, repeats
similar println! statements, which is less maintainable and more prone to
errors.
<Memo>
Rust's macro system is hygienic, meaning it avoids name conflicts between
variables in the macro and the calling code.
100
Refactor common patterns into reusable
functions or modules
Extracting common code patterns into functions or modules improves code
organization and readability.
use std::fs::File;
use std::io::{self, BufRead, BufReader};
// Reusable function to process files
fn process_file(filename: &str) -> io::Result<Vec<String>> {
let file = File::open(filename)?;
let reader = BufReader::new(file);
let lines: Vec<String> = reader.lines().filter_map(Result::ok).collect();
Ok(lines)
}
fn main() -> io::Result<()> {
// Process multiple files using the reusable function
let files = vec!["file1.txt", "file2.txt", "file3.txt"];
for file in files {
match process_file(file) {
Ok(lines) => println!("Processed {} lines from {}", lines.len(), file),
Err(e) => eprintln!("Error processing {}: {}", file, e),
}
}
Ok(())
}
<Bad Code>
use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn main() -> io::Result<()> {
// Repetitive code for processing multiple files
let file1 = File::open("file1.txt")?;
let reader1 = BufReader::new(file1);
let lines1: Vec<String> = reader1.lines().filter_map(Result::ok).collect();
println!("Processed {} lines from file1.txt", lines1.len());
let file2 = File::open("file2.txt")?;
let reader2 = BufReader::new(file2);
let lines2: Vec<String> = reader2.lines().filter_map(Result::ok).collect();
println!("Processed {} lines from file2.txt", lines2.len());
let file3 = File::open("file3.txt")?;
let reader3 = BufReader::new(file3);
let lines3: Vec<String> = reader3.lines().filter_map(Result::ok).collect();
println!("Processed {} lines from file3.txt", lines3.len());
Ok(())
}
The good code example refactors the file processing logic into a separate
function, process_file. This approach eliminates code duplication and makes
the main function cleaner and more maintainable. It also allows for easier
error handling and processing of multiple files in a loop. The bad code
example repeats the same file processing logic for each file, leading to
verbose and hard-to-maintain code.
<Memo>
Rust's module system allows for organizing code into logical units,
improving code structure and reusability across projects.
101
Use crate and pub keywords to control
visibility and reuse.
Effectively manage the visibility of your Rust code using the crate and pub
keywords to ensure modularity and reusability.
In Rust, the crate and pub keywords are essential for controlling the
visibility of functions, structs, and modules. Proper use of these keywords
can make your code more modular and easier to maintain.
< Good Code >
// src/lib.rs
// Define a module
mod utils {
// This function is private to the module
fn private_function() {
println!("This is a private function.");
}
// This function is public within the crate
pub(crate) fn crate_function() {
println!("This function is public within the crate.");
}
// This function is public to all external crates
pub fn public_function() {
println!("This function is public to all external crates.");
}
}
// Public function in the root of the crate
pub fn root_function() {
println!("This is a public function in the root of the crate.");
}
<Bad Code>
// src/lib.rs
// Define a module
mod utils {
// This function is private to the module
fn private_function() {
println!("This is a private function.");
}
// This function is public within the crate
pub fn crate_function() {
println!("This function is public within the crate.");
}
// This function is public to all external crates
pub fn public_function() {
println!("This function is public to all external crates.");
}
}
// Public function in the root of the crate
fn root_function() {
println!("This is a public function in the root of the crate.");
}
In the good example, the crate keyword is used to limit the visibility of
crate_function to within the crate, while pub is used to make
public_function accessible to external crates. The root_function is also
correctly marked as pub to be accessible outside the crate. In the bad
example, crate_function is unnecessarily public, and root_function is not
accessible outside the crate due to the missing pub keyword.
<Memo>
The pub(crate) visibility modifier is useful for creating internal APIs that
should not be exposed to external crates, helping to maintain encapsulation
and reduce the risk of breaking changes.
102
Leverage Cargo workspaces to manage
multiple related packages.
Use Cargo workspaces to efficiently manage and build multiple related Rust
packages in a single repository.
<Memo>
Cargo workspaces are particularly useful for large projects with multiple
interdependent packages, as they allow for more efficient builds and easier
dependency management.
Afterword
◆
Thank you for taking the time to read through this book.
I hope you found the 100 techniques for writing more readable Rust code
both insightful and practical.
By leveraging Rust's unique features and idiomatic practices, this book
aimed to guide you in crafting code that is not only functional but also easy
for others to understand and maintain.
If you have specific topics you would like to see covered in future books,
please let me know.
Your suggestions are always welcome and help me to better serve the needs
of the Rust programming community.
Even if you are pressed for time, a simple star rating would be greatly
appreciated.
Your feedback not only helps me improve but also assists other readers in
finding valuable resources.