0% found this document useful (0 votes)
22 views236 pages

Rust 100 Exercises in Readable Code 100 Techniques For Writing Readable Rust Code (Kanro Tomoya)

This document is a guide to 100 techniques for writing more readable code in Rust, emphasizing idiomatic practices and unique features of the language. It covers various topics such as pattern matching, error handling with Option and Result types, and the use of enums and traits for better code organization and readability. Each technique includes examples of good and bad code to illustrate best practices for clean and maintainable Rust programming.

Uploaded by

mingcoder2025
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
22 views236 pages

Rust 100 Exercises in Readable Code 100 Techniques For Writing Readable Rust Code (Kanro Tomoya)

This document is a guide to 100 techniques for writing more readable code in Rust, emphasizing idiomatic practices and unique features of the language. It covers various topics such as pattern matching, error handling with Option and Result types, and the use of enums and traits for better code organization and readability. Each technique includes examples of good and bad code to illustrate best practices for clean and maintainable Rust programming.

Uploaded by

mingcoder2025
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 236

Index

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.

Each technique is accompanied by examples of both good and bad code,


making it straightforward to grasp the principles of writing clean and
maintainable Rust code.

We hope this book serves as a valuable resource in your journey to


becoming a more proficient Rust programmer.
1
Use pattern matching with match for
clear and concise control flow.
Pattern matching with match provides a clear and concise way to handle
different cases in Rust.

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 >

fn divide(a: f64, b: f64) -> Result<f64, &'static str> {


if b == 0.0 {
Err("Cannot divide by zero")
} else {
Ok(a / b)
}
}
fn main() {
match divide(4.0, 2.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
match divide(4.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
}

<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 >

// Define a vector of integers


let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Use iter, filter, and map to process the vector
let result: Vec<i32> = numbers.iter()
.filter(|&&x| x % 2 == 0) // Filter even numbers
.map(|&x| x * x) // Square each number
.collect(); // Collect the results into a new vector
// Print the result
println!("{:?}", result); // Output: [4, 16, 36, 64, 100]

<Bad Code>

// Define a vector of integers


let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Create an empty vector to store the results
let mut result = Vec::new();
// Iterate over the vector using a for loop
for i in &numbers {
if i % 2 == 0 { // Check if the number is even
result.push(i * i); // Square the number and add it to the result vector
}
}
// Print the result
println!("{:?}", result); // Output: [4, 16, 36, 64, 100]

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 >

// Define an enum to represent different shapes


enum Shape {
Circle(f64), // Circle with a radius
Rectangle(f64, f64), // Rectangle with width and height
Triangle(f64, f64, f64), // Triangle with three sides
}
// Function to calculate the area of a shape
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => std::f64::consts::PI * radius * radius, // Area
of a circle
Shape::Rectangle(width, height) => width * height, // Area of a
rectangle
Shape::Triangle(a, b, c) => {
// Using Heron's formula to calculate the area of a triangle
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
// Example usage
let circle = Shape::Circle(5.0);
let rectangle = Shape::Rectangle(4.0, 6.0);
let triangle = Shape::Triangle(3.0, 4.0, 5.0);
println!("Circle area: {}", area(&circle)); // Output: Circle area:
78.53981633974483
println!("Rectangle area: {}", area(&rectangle)); // Output: Rectangle area:
24
println!("Triangle area: {}", area(&triangle)); // Output: Triangle area: 6

<Bad Code>

// Define constants to represent different shapes


const CIRCLE: u8 = 1;
const RECTANGLE: u8 = 2;
const TRIANGLE: u8 = 3;
// Function to calculate the area of a shape
fn area(shape_type: u8, dimensions: &[f64]) -> f64 {
match shape_type {
CIRCLE => std::f64::consts::PI * dimensions[0] * dimensions[0], //
Area of a circle
RECTANGLE => dimensions[0] * dimensions[1], // Area of a
rectangle
TRIANGLE => {
// Using Heron's formula to calculate the area of a triangle
let s = (dimensions[0] + dimensions[1] + dimensions[2]) / 2.0;
(s * (s - dimensions[0]) * (s - dimensions[1]) * (s -
dimensions[2])).sqrt()
}
_ => 0.0, // Default case for unknown shape types
}
}
// Example usage
let circle_area = area(CIRCLE, &[5.0]);
let rectangle_area = area(RECTANGLE, &[4.0, 6.0]);
let triangle_area = area(TRIANGLE, &[3.0, 4.0, 5.0]);
println!("Circle area: {}", circle_area); // Output: Circle area:
78.53981633974483
println!("Rectangle area: {}", rectangle_area); // Output: Rectangle area: 24
println!("Triangle area: {}", triangle_area); // Output: Triangle area: 6

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 >

// Define a trait with a shared behavior


trait Describable {
fn describe(&self) -> String;
}
// Implement the trait for a struct
struct Dog {
name: String,
age: u8,
}
impl Describable for Dog {
fn describe(&self) -> String {
format!("Dog named {} is {} years old.", self.name, self.age)
}
}
// Implement the trait for another struct
struct Car {
model: String,
year: u16,
}
impl Describable for Car {
fn describe(&self) -> String {
format!("Car model {} from year {}.", self.model, self.year)
}
}
fn main() {
let my_dog = Dog { name: String::from("Buddy"), age: 3 };
let my_car = Car { model: String::from("Tesla"), year: 2020 };
// Use the shared behavior
println!("{}", my_dog.describe());
println!("{}", my_car.describe());
}

<Bad Code>

// No trait used, repetitive code


struct Dog {
name: String,
age: u8,
}
impl Dog {
fn describe(&self) -> String {
format!("Dog named {} is {} years old.", self.name, self.age)
}
}
struct Car {
model: String,
year: u16,
}
impl Car {
fn describe(&self) -> String {
format!("Car model {} from year {}.", self.model, self.year)
}
}
fn main() {
let my_dog = Dog { name: String::from("Buddy"), age: 3 };
let my_car = Car { model: String::from("Tesla"), year: 2020 };
// Repetitive code without shared behavior
println!("{}", my_dog.describe());
println!("{}", my_car.describe());
}

In the good example, the Describable trait is defined to encapsulate the


shared behavior of describing an object. This trait is then implemented for
both Dog and Car structs, allowing them to share the describe method. This
approach promotes code reusability and readability. In the bad example, the
describe method is implemented separately for each struct without using a
trait, leading to repetitive and less maintainable 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.

Descriptive names help other developers quickly grasp the purpose of


variables and functions, reducing the cognitive load and potential for errors.
This example illustrates the importance of naming conventions.
< Good Code >

// Descriptive variable and function names


fn calculate_area_of_rectangle(width: f64, height: f64) -> f64 {
width * height
}
fn main() {
let rectangle_width = 5.0;
let rectangle_height = 10.0;
let area = calculate_area_of_rectangle(rectangle_width,
rectangle_height);
println!("The area of the rectangle is {}", area);
}

<Bad Code>

// Non-descriptive variable and function names


fn calc(w: f64, h: f64) -> f64 {
w*h
}
fn main() {
let w = 5.0;
let h = 10.0;
let a = calc(w, h);
println!("The area is {}", a);
}

In the good example, the function calculate_area_of_rectangle and the


variables rectangle_width and rectangle_height are named descriptively,
clearly conveying their purpose. This makes the code easier to read and
understand. In the bad example, the function calc and the variables w, h,
and a are not descriptive, making it harder to understand what the code does
without additional context.

<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.

Adopting a consistent naming convention like snake_case helps maintain


readability and uniformity in your Rust codebase. This practice aligns with
Rust's community standards and makes it easier for others to understand
and contribute to your code.
< Good Code >

// Good example using snake_case


fn calculate_area(width: f64, height: f64) -> f64 {
width * height
}
let rectangle_width = 10.0;
let rectangle_height = 5.0;
let area = calculate_area(rectangle_width, rectangle_height);
println!("The area of the rectangle is: {}", area);

<Bad Code>

// Bad example using inconsistent naming


fn calculateArea(width: f64, height: f64) -> f64 {
width * height
}
let rectangleWidth = 10.0;
let rectangleHeight = 5.0;
let area = calculateArea(rectangleWidth, rectangleHeight);
println!("The area of the rectangle is: {}", area);

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 >

// Good example using boolean prefixes


fn is_even(number: i32) -> bool {
number % 2 == 0
}
fn has_permission(user_role: &str) -> bool {
user_role == "admin"
}
fn can_drive(age: u32) -> bool {
age >= 18
}
let number = 4;
let user_role = "admin";
let age = 20;
println!("Is the number even? {}", is_even(number));
println!("Does the user have permission? {}", has_permission(user_role));
println!("Can the person drive? {}", can_drive(age));

<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));

In the good example, the boolean functions is_even, has_permission, and


can_drive use prefixes that clearly indicate their purpose, making the code
more readable and self-explanatory. In the bad example, the function names
even, permission, and drive lack these prefixes, making it less clear that
they return boolean values and what their purpose is.

<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 >

// Using `const` for compile-time constants


const MAX_USERS: u32 = 1000;
// Using `static` for global constants
static APP_NAME: &str = "MyApp";
fn main() {
println!("Max users: {}", MAX_USERS);
println!("App name: {}", APP_NAME);
}

<Bad Code>

// Not using `const` or `static` for constants


let max_users: u32 = 1000;
let app_name: &str = "MyApp";
fn main() {
println!("Max users: {}", max_users);
println!("App name: {}", app_name);
}
In the good example, const and static are used to define constants, making it
clear that these values are immutable. This helps in understanding the code
better and ensures that these values are not accidentally modified. In the bad
example, using let for constants can lead to confusion and potential bugs, as
it suggests that these values might change.

<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 >

// Good example: Using clear and descriptive names


fn calculate_total_price(item_price: f64, tax_rate: f64) -> f64 {
let total_price = item_price + (item_price * tax_rate);
total_price
}
fn main() {
let price = calculate_total_price(100.0, 0.08);
println!("Total price: {}", price);
}

<Bad Code>

// Bad example: Using unclear abbreviations


fn calc_tp(ip: f64, tr: f64) -> f64 {
let tp = ip + (ip * tr);
tp
}
fn main() {
let p = calc_tp(100.0, 0.08);
println!("Total price: {}", p);
}

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 >

// Good example: Using full words for variable names


fn calculate_area_of_rectangle(length: f64, width: f64) -> f64 {
let area = length * width;
area
}
fn main() {
let length = 5.0;
let width = 3.0;
let area = calculate_area_of_rectangle(length, width);
println!("Area of the rectangle: {}", area);
}

<Bad Code>

// Bad example: Using single letters for variable names


fn calc_area(l: f64, w: f64) -> f64 {
let a = l * w;
a
}
fn main() {
let l = 5.0;
let w = 3.0;
let a = calc_area(l, w);
println!("Area of the rectangle: {}", a);
}

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 >

// Good example: Distinct and descriptive variable names


let user_age = 25; // Age of the user
let user_name = "Alice"; // Name of the user
let user_email = "[email protected]"; // Email of the user
println!("User: {}, Age: {}, Email: {}", user_name, user_age, user_email);

<Bad Code>

// Bad example: Similar and confusing variable names


let age1 = 25; // Age of the user
let age2 = "Alice"; // Name of the user
let age3 = "[email protected]"; // Email of the user
println!("User: {}, Age: {}, Email: {}", age2, age1, age3);

In the good example, the variable names user_age, user_name, and


user_email clearly describe their purpose, making the code easy to read and
understand. In the bad example, the similar names age1, age2, and age3 are
confusing and do not convey the variables' purposes, making the code
harder to follow and more prone to errors.

<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 >

// Good example: Descriptive function names


fn calculate_area(radius: f64) -> f64 {
// Calculate the area of a circle
3.14159 * radius * radius
}
fn print_greeting(name: &str) {
// Print a greeting message
println!("Hello, {}!", name);
}
let area = calculate_area(5.0);
print_greeting("Alice");

<Bad Code>

// Bad example: Vague function names


fn calc(r: f64) -> f64 {
// Calculate the area of a circle
3.14159 * r * r
}
fn pg(n: &str) {
// Print a greeting message
println!("Hello, {}!", n);
}
let a = calc(5.0);
pg("Alice");

In the good example, the function names calculate_area and print_greeting


clearly describe their actions, making the code easy to understand. In the
bad example, the vague names calc and pg do not provide enough
information about what the functions do, making the code harder to read
and understand.

<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.

Consistent naming conventions help developers understand the purpose and


usage of variables, functions, and types at a glance. This practice reduces
cognitive load and makes the codebase easier to navigate.
< Good Code >

// Good example of consistent naming conventions


// Struct representing a user
struct User {
first_name: String,
last_name: String,
email: String,
}
// Function to create a new user
impl User {
fn new(first_name: &str, last_name: &str, email: &str) -> Self {
Self {
first_name: first_name.to_string(),
last_name: last_name.to_string(),
email: email.to_string(),
}
}
// Function to get the user's full name
fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}

<Bad Code>

// Bad example of inconsistent naming conventions


// Struct representing a user
struct user {
FirstName: String,
lastName: String,
Email: String,
}
// Function to create a new user
impl user {
fn createUser(first: &str, last: &str, mail: &str) -> Self {
Self {
FirstName: first.to_string(),
lastName: last.to_string(),
Email: mail.to_string(),
}
}
// Function to get the user's full name
fn getFullName(&self) -> String {
format!("{} {}", self.FirstName, self.lastName)
}
}

In the good example, the naming conventions are consistent: first_name,


last_name, and email use snake_case, which is the standard in Rust. The
method names new and full_name are also in snake_case. In the bad
example, the struct name user is in lowercase, and the field names use
inconsistent casing (FirstName, lastName, Email). The method names
createUser and getFullName use camelCase, which is not idiomatic in Rust.

<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 >

// Good example of using impl blocks to group related methods


// Struct representing a rectangle
struct Rectangle {
width: u32,
height: u32,
}
// Implementation block for Rectangle methods
impl Rectangle {
// Method to create a new rectangle
fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
// Method to calculate the area of the rectangle
fn area(&self) -> u32 {
self.width * self.height
}
// Method to check if the rectangle can hold another rectangle
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

<Bad Code>

// Bad example of not using impl blocks properly


// Struct representing a rectangle
struct Rectangle {
width: u32,
height: u32,
}
// Function to create a new rectangle
fn create_rectangle(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
// Function to calculate the area of the rectangle
fn rectangle_area(rect: &Rectangle) -> u32 {
rect.width * rect.height
}
// Function to check if the rectangle can hold another rectangle
fn rectangle_can_hold(rect: &Rectangle, other: &Rectangle) -> bool {
rect.width > other.width && rect.height > other.height
}

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 >

// Implementing the From trait for a custom type


struct MyType {
value: i32,
}
impl From<i32> for MyType {
fn from(item: i32) -> Self {
MyType { value: item }
}
}
fn main() {
let num = 42;
// Using the From trait to convert i32 to MyType
let my_value: MyType = MyType::from(num);
println!("MyType value: {}", my_value.value);
// Using the Into trait to convert MyType back to i32
let original_value: i32 = my_value.into();
println!("Original value: {}", original_value);
}
<Bad Code>

// Manual conversion without using From or Into traits


struct MyType {
value: i32,
}
fn main() {
let num = 42;
// Manual conversion from i32 to MyType
let my_value = MyType { value: num };
println!("MyType value: {}", my_value.value);
// Manual conversion from MyType to i32
let original_value = my_value.value;
println!("Original value: {}", original_value);
}

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>

fn process_data(input: &str) -> String {


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");
owned_data
} else {
// Otherwise, we still clone the input data unnecessarily
input.to_string()
}
}
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);
}

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.

In Rust, Rc (Reference Counted) and Arc (Atomic Reference Counted) are


smart pointers used to enable multiple ownership of data. Rc is used in
single-threaded scenarios, while Arc is thread-safe and can be used in multi-
threaded contexts.
< Good Code >

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);
}

In the good example, Rc is used for single-threaded scenarios, and Arc is


used for multi-threaded scenarios, ensuring proper memory management
and thread safety. In the bad example, Rc is incorrectly used in a multi-
threaded context, which would result in a compile-time error. Additionally,
using Arc in a single-threaded context is unnecessary and adds overhead.

<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.

RefCell provides interior mutability in single-threaded contexts, allowing


for mutable access to data even when it is behind an immutable reference.
Mutex provides thread-safe interior mutability, ensuring that only one
thread can access the data at a time.
< Good Code >

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());
}

In the good example, RefCell is used for single-threaded interior mutability,


and Mutex is used for thread-safe interior mutability, ensuring proper data
access and mutation. In the bad example, RefCell is incorrectly used in a
multi-threaded context, which would result in a runtime panic. Additionally,
using Mutex in a single-threaded context is unnecessary and adds overhead.

<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.

Understanding the purpose of complex algorithms or data structures can


help developers appreciate their use and maintainability in Rust code.
< Good Code >

// This function uses a B-tree to store and retrieve data efficiently.


// B-trees are balanced, allowing for fast insertions, deletions, and lookups.
use std::collections::BTreeMap;
fn main() {
let mut btree = BTreeMap::new();
btree.insert(1, "one");
btree.insert(2, "two");
btree.insert(3, "three");
// Efficiently retrieve values
if let Some(value) = btree.get(&2) {
println!("The value for key 2 is: {}", value);
}
}

<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.

Non-obvious design decisions often have underlying reasons that improve


code maintainability, performance, or readability. Understanding these
reasons can help developers appreciate and follow the design choices.
< Good Code >

// This function uses an enum to represent different states of a process.


// Using an enum makes the code more readable and maintainable.
enum ProcessState {
Started,
InProgress,
Completed,
Failed,
}
fn main() {
let state = ProcessState::InProgress;
match state {
ProcessState::Started => println!("Process has started."),
ProcessState::InProgress => println!("Process is in progress."),
ProcessState::Completed => println!("Process is completed."),
ProcessState::Failed => println!("Process has failed."),
}
}

<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."),
}
}

The good example uses an enum to represent different states of a process,


which makes the code more readable and maintainable. Enums provide a
clear and type-safe way to handle different states, reducing the risk of
errors. The bad example uses integers to represent states, which can be
confusing and error-prone, as it relies on magic numbers without context.

<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.

Providing clear documentation for the expected input and output of


functions helps other developers understand how to use your code correctly
and efficiently.
< Good Code >

/// Adds two numbers together.


///
/// # Arguments
///
/// * `a` - A 32-bit integer.
/// * `b` - A 32-bit integer.
///
/// # Returns
///
/// * A 32-bit integer that is the sum of `a` and `b`.
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
}

<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
}

In the good example, the function add is well-documented with a


description of its purpose, the arguments it takes, and the value it returns.
This makes it easier for other developers to understand and use the function
correctly. In the bad example, the lack of documentation can lead to
confusion about what the function does and how to use it.

<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.

When using unsafe code in Rust, it is crucial to document why it is


necessary and what measures are taken to ensure safety, as unsafe code can
bypass Rust's safety checks.
< Good Code >

/// Copies bytes from one location to another.


///
/// # Safety
///
/// This function is unsafe because it performs raw pointer dereferencing.
/// The caller must ensure that `src` and `dst` are valid for reads and writes
/// of `count` bytes, and that the memory regions do not overlap.
unsafe fn copy_bytes(src: *const u8, dst: *mut u8, count: usize) {
for i in 0..count {
*dst.add(i) = *src.add(i);
}
}
fn main() {
let src = [1, 2, 3, 4];
let mut dst = [0; 4];
unsafe {
copy_bytes(src.as_ptr(), dst.as_mut_ptr(), src.len());
}
println!("Copied bytes: {:?}", dst); // Copied bytes: [1, 2, 3, 4]
}
<Bad Code>

unsafe fn copy_bytes(src: *const u8, dst: *mut u8, count: usize) {


for i in 0..count {
*dst.add(i) = *src.add(i);
}
}
fn main() {
let src = [1, 2, 3, 4];
let mut dst = [0; 4];
unsafe {
copy_bytes(src.as_ptr(), dst.as_mut_ptr(), src.len());
}
println!("Copied bytes: {:?}", dst); // Copied bytes: [1, 2, 3, 4]
}

In the good example, the copy_bytes function is well-documented with a


clear explanation of why it is unsafe and what the caller must ensure to
maintain safety. This helps other developers understand the risks and
responsibilities when using the function. In the bad example, the lack of
documentation can lead to misuse and potential safety issues, as other
developers may not be aware of the necessary precautions.

<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.

Using doc comments helps create clear and comprehensive documentation


directly from your code, which is essential for maintaining and sharing your
Rust projects.
< Good Code >

/// Adds two numbers together.


///
/// # Examples
///
/// ```
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a+b
}

<Bad Code>

// Adds two numbers together.


pub fn add(a: i32, b: i32) -> i32 {
a+b
}
In the good example, the use of /// provides a clear description and example
of the function, which can be automatically included in generated
documentation. The bad example uses regular comments, which are not
included in the generated documentation and lack detailed information.

<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.

Keeping comments synchronized with code changes is crucial for


maintaining the accuracy and usefulness of the comments, which helps
other developers understand the code better.
< Good Code >

/// Calculates the factorial of a number.


///
/// # Examples
///
/// ```
/// let result = factorial(5);
/// assert_eq!(result, 120);
/// ```
pub fn factorial(n: u32) -> u32 {
if n == 0 {
1
} else {
n * factorial(n - 1)
}
}

<Bad Code>

/// Calculates the factorial of a number.


///
/// # Examples
///
/// ```
/// let result = factorial(5);
/// assert_eq!(result, 120);
/// ```
pub fn factorial(n: u32) -> u32 {
if n == 0 {
1
} else {
// Incorrectly updated code without updating the comment
n + factorial(n - 1)
}
}

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.

When writing comments, ensure they provide additional context or


clarification rather than simply restating what the code does.
< Good Code >

// Calculate the factorial of a number using recursion


fn factorial(n: u32) -> u32 {
if n == 0 {
1 // Base case: 0! is 1
} else {
n * factorial(n - 1) // Recursive case
}
}

<Bad Code>

// This function calculates the factorial of a number


fn factorial(n: u32) -> u32 {
// If n is 0, return 1
if n == 0 {
1 // Return 1
} else {
// Multiply n by the factorial of (n - 1)
n * factorial(n - 1) // Recursive call
}
}

In the good example, comments provide meaningful context, such as


explaining the base and recursive cases. In the bad example, comments
merely restate what the code is doing, which is redundant and unhelpful.

<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 >

// Using a HashMap to count occurrences of each character in a string


// This approach is chosen for its average O(1) time complexity for
insertions and lookups
use std::collections::HashMap;
fn char_count(s: &str) -> HashMap<char, u32> {
let mut counts = HashMap::new();
for c in s.chars() {
*counts.entry(c).or_insert(0) += 1;
}
counts
}

<Bad Code>

// This function counts the occurrences of each character in a string


use std::collections::HashMap;
fn char_count(s: &str) -> HashMap<char, u32> {
let mut counts = HashMap::new();
for c in s.chars() {
// Insert the character into the HashMap or update its count
*counts.entry(c).or_insert(0) += 1;
}
counts
}

In the good example, the comment explains why a HashMap is used,


providing insight into the choice of data structure. In the bad example, the
comment only describes what the code does, which is already clear from the
code itself.

<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 >

// Calculate the factorial of a number using recursion


fn factorial(n: u32) -> u32 {
if n == 0 {
1
} else {
n * factorial(n - 1)
}
}

<Bad Code>

// This function calculates the factorial of a number by using recursion.


// It takes a single argument 'n' which is an unsigned 32-bit integer.
// If 'n' is zero, it returns 1 because the factorial of zero is 1.
// Otherwise, it multiplies 'n' by the factorial of 'n-1'.
// This process continues until 'n' is zero.
fn factorial(n: u32) -> u32 {
if n == 0 {
1
} else {
n * factorial(n - 1)
}
}

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.

TODO comments serve as reminders for developers to address specific


issues or improvements in the future, ensuring that important tasks are not
forgotten.
< Good Code >

fn process_data(data: &str) -> Result<(), String> {


// TODO: Optimize this function for large datasets
if data.is_empty() {
return Err("Data is empty".to_string());
}
// Process data...
Ok(())
}

<Bad Code>

fn process_data(data: &str) -> Result<(), String> {


// This function processes the data and returns a Result.
// If the data is empty, it returns an error.
// Otherwise, it processes the data and returns Ok.
if data.is_empty() {
return Err("Data is empty".to_string());
}
// Process data...
Ok(())
}

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
}
}

In the good example, the ? operator is used to propagate errors


automatically, making the code more concise and readable. In the bad
example, errors are handled manually with match statements, which adds
unnecessary verbosity and complexity.

<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.

The async and await keywords in Rust enable writing asynchronous


functions and handling asynchronous operations in a way that resembles
synchronous code, improving readability and maintainability.
< Good Code >

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 >

// Define a trait for a common behavior


trait Shape {
fn area(&self) -> f64;
}
// Implement the trait for a struct
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
// Function that takes a Box<dyn Shape> to allow for dynamic dispatch
fn print_area(shape: Box<dyn Shape>) {
println!("The area is {}", shape.area());
}
fn main() {
let circle = Circle { radius: 5.0 };
let boxed_circle: Box<dyn Shape> = Box::new(circle);
print_area(boxed_circle);
}

<Bad Code>

// Define a trait for a common behavior


trait Shape {
fn area(&self) -> f64;
}
// Implement the trait for a struct
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
// Function that takes a reference to a trait object
fn print_area(shape: &dyn Shape) {
println!("The area is {}", shape.area());
}
fn main() {
let circle = Circle { radius: 5.0 };
print_area(&circle); // This works but does not use heap allocation
}

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.

fmt is a tool that automatically formats Rust code according to style


guidelines. Consistent formatting helps improve code readability and
maintainability, making it easier for others to understand and contribute to
your codebase.
< Good Code >

// This code is formatted using rustfmt


fn main() {
let numbers = vec![1, 2, 3, 4, 5];
for number in numbers {
println!("{}", number);
}
}

<Bad Code>

// This code is not formatted using rustfmt


fn main() {
let numbers = vec![1,2,3,4,5];
for number in numbers {
println!("{}", number);
}
}
In the good example, the code is formatted using rustfmt, which ensures
consistent indentation and spacing, making it easier to read. In the bad
example, the code lacks proper formatting, which can make it harder to
follow and maintain.

<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.

Organizing code into modules and submodules helps in managing


complexity by grouping related functionalities together. This makes the
codebase easier to navigate and understand.
< Good Code >

// 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);
}

In the good example, the code is organized into logical modules


(math_operations, arithmetic, and algebra), making it easier to understand
and maintain. In the bad example, all functions are placed directly in the
main file, which can become unwieldy as the codebase grows.

<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.

Whitespace and indentation are crucial for making code readable.


Consistent formatting helps other developers understand the structure and
flow of the code quickly.
< Good 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 >

// Function to add two numbers


fn add(a: i32, b: i32) -> i32 {
a+b
}
// Function to subtract two numbers
fn subtract(a: i32, b: i32) -> i32 {
a-b
}
// Main function
fn main() {
let sum = add(5, 3);
let difference = subtract(5, 3);
println!("Sum: {}", sum);
println!("Difference: {}", difference);
}

<Bad Code>

fn add(a: i32, b: i32) -> i32 { a + b }


fn subtract(a: i32, b: i32) -> i32 { a - b }
fn main() { let sum = add(5, 3); let difference = subtract(5, 3); println!
("Sum: {}", sum); println!("Difference: {}", difference); }

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.

Deeply nested code can be difficult to read and understand. Breaking it


down into smaller functions makes the code more modular and easier to
follow.
< Good Code >

// Function to check if a number is even


fn is_even(num: i32) -> bool {
num % 2 == 0
}
// Function to print even numbers in a range
fn print_even_numbers(range: std::ops::Range<i32>) {
for num in range {
if is_even(num) {
println!("{} is even", num);
}
}
}
fn main() {
print_even_numbers(1..10);
}

<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 >

// Good example using `if let`


fn main() {
let some_option = Some(5);
// Using `if let` to concisely check and use the value inside the option
if let Some(value) = some_option {
println!("The value is: {}", value);
} else {
println!("No value found");
}
}
// Good example using `while let`
fn process_numbers(numbers: &mut Vec<i32>) {
// Using `while let` to process elements until the vector is empty
while let Some(number) = numbers.pop() {
println!("Processing number: {}", number);
}
}

<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 >

// Good example using `for` loop


fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Using `for` loop to iterate over the vector
for number in numbers.iter() {
println!("The number is: {}", number);
}
}

<Bad Code>

// Bad example using `while` loop


fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let mut index = 0;
// Using `while` loop to iterate over the vector
while index < numbers.len() {
println!("The number is: {}", numbers[index]);
index += 1;
}
}

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 >

fn find_first_even(numbers: &[i32]) -> Option<i32> {


for &num in numbers {
if num % 2 == 0 {
return Some(num); // Found the first even number, break the loop
}
}
None // No even number found
}
fn skip_negatives_and_sum(numbers: &[i32]) -> i32 {
let mut sum = 0;
for &num in numbers {
if num < 0 {
continue; // Skip negative numbers
}
sum += num;
}
sum
}
fn main() {
let numbers = [1, 3, 5, 6, 7, -2, 8];
if let Some(even) = find_first_even(&numbers) {
println!("First even number: {}", even);
} else {
println!("No even number found.");
}
let sum = skip_negatives_and_sum(&numbers);
println!("Sum of non-negative numbers: {}", sum);
}

<Bad Code>

fn find_first_even(numbers: &[i32]) -> Option<i32> {


for &num in numbers {
if num % 2 == 0 {
return Some(num); // Found the first even number, break the loop
} else {
// Do nothing, just continue
}
}
None // No even number found
}
fn skip_negatives_and_sum(numbers: &[i32]) -> i32 {
let mut sum = 0;
for &num in numbers {
if num >= 0 {
sum += num;
} else {
// Do nothing, just continue
}
}
sum
}
fn main() {
let numbers = [1, 3, 5, 6, 7, -2, 8];
if let Some(even) = find_first_even(&numbers) {
println!("First even number: {}", even);
} else {
println!("No even number found.");
}
let sum = skip_negatives_and_sum(&numbers);
println!("Sum of non-negative numbers: {}", sum);
}

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 >

fn process_data(data: Option<&str>) -> Result<(), &'static str> {


// Check for error condition early
let data = match data {
Some(d) => d,
None => return Err("No data provided"),
};
// Proceed with processing
if data.is_empty() {
return Err("Data is empty");
}
println!("Processing data: {}", data);
Ok(())
}

<Bad Code>

fn process_data(data: Option<&str>) -> Result<(), &'static str> {


if data.is_some() {
let data = data.unwrap();
if !data.is_empty() {
println!("Processing data: {}", data);
Ok(())
} else {
Err("Data is empty")
}
} else {
Err("No data provided")
}
}

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.

Match guards are additional conditions specified after a pattern in a match


arm. They allow you to refine the match logic by adding extra checks,
making the code more readable and expressive.
< Good Code >

fn classify_number(num: i32) -> &'static str {


match num {
n if n < 0 => "Negative",
n if n == 0 => "Zero",
n if n > 0 && n % 2 == 0 => "Positive even",
n if n > 0 && n % 2 != 0 => "Positive odd",
_ => "Unknown",
}
}

<Bad Code>

fn classify_number(num: i32) -> &'static str {


match num {
n if n < 0 => "Negative",
n if n == 0 => "Zero",
n if n > 0 => {
if n % 2 == 0 {
"Positive even"
} else {
"Positive odd"
}
},
_ => "Unknown",
}
}

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 >

// Function to parse a string to an integer and then double it


fn double_number(input: &str) -> Result<i32, std::num::ParseIntError> {
input.parse::<i32>().map(|n| n * 2)
}
// Function to parse a string to an integer, double it, and then convert to a
string
fn double_and_stringify(input: &str) -> Result<String,
std::num::ParseIntError> {
input.parse::<i32>().and_then(|n| Ok((n * 2).to_string()))
}
fn main() {
match double_number("10") {
Ok(n) => println!("Doubled number: {}", n),
Err(e) => println!("Error: {}", e),
}
match double_and_stringify("10") {
Ok(s) => println!("Doubled and stringified: {}", s),
Err(e) => println!("Error: {}", e),
}
}
<Bad Code>

// Function to parse a string to an integer and then double it


fn double_number(input: &str) -> Result<i32, std::num::ParseIntError> {
match input.parse::<i32>() {
Ok(n) => Ok(n * 2),
Err(e) => Err(e),
}
}
// Function to parse a string to an integer, double it, and then convert to a
string
fn double_and_stringify(input: &str) -> Result<String,
std::num::ParseIntError> {
match input.parse::<i32>() {
Ok(n) => Ok((n * 2).to_string()),
Err(e) => Err(e),
}
}
fn main() {
match double_number("10") {
Ok(n) => println!("Doubled number: {}", n),
Err(e) => println!("Error: {}", e),
}
match double_and_stringify("10") {
Ok(s) => println!("Doubled and stringified: {}", s),
Err(e) => println!("Error: {}", e),
}
}

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.

In Rust, unwrap_or and unwrap_or_else are methods that allow you to


provide default values when dealing with Option and Result types, making
your code more robust and readable.
< Good Code >

// Function to get the length of a string or return a default value


fn get_length_or_default(input: Option<&str>) -> usize {
input.map(|s| s.len()).unwrap_or(0)
}
// Function to parse a string to an integer or return a default value
fn parse_or_default(input: &str) -> i32 {
input.parse::<i32>().unwrap_or_else(|_| 0)
}
fn main() {
let length = get_length_or_default(Some("Hello"));
println!("Length: {}", length); // Output: Length: 5
let length = get_length_or_default(None);
println!("Length: {}", length); // Output: Length: 0
let number = parse_or_default("42");
println!("Number: {}", number); // Output: Number: 42
let number = parse_or_default("not a number");
println!("Number: {}", number); // Output: Number: 0
}
<Bad Code>

// Function to get the length of a string or return a default value


fn get_length_or_default(input: Option<&str>) -> usize {
match input {
Some(s) => s.len(),
None => 0,
}
}
// Function to parse a string to an integer or return a default value
fn parse_or_default(input: &str) -> i32 {
match input.parse::<i32>() {
Ok(n) => n,
Err(_) => 0,
}
}
fn main() {
let length = get_length_or_default(Some("Hello"));
println!("Length: {}", length); // Output: Length: 5
let length = get_length_or_default(None);
println!("Length: {}", length); // Output: Length: 0
let number = parse_or_default("42");
println!("Number: {}", number); // Output: Number: 42
let number = parse_or_default("not a number");
println!("Number: {}", number); // Output: Number: 0
}

The good example uses unwrap_or and unwrap_or_else to handle default


values in a concise manner. This makes the code more readable and reduces
the need for verbose match statements. The bad example uses match
statements, which are more verbose and can make the code harder to read.

<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 >

// Function to get the length of a string if it exists


fn get_length(opt: Option<&str>) -> Option<usize> {
opt.map(|s| s.len())
}
// Function to get the first character of a string if it exists
fn get_first_char(opt: Option<&str>) -> Option<char> {
opt.and_then(|s| s.chars().next())
}
fn main() {
let some_string = Some("Hello, Rust!");
let none_string: Option<&str> = None;
// Using map to get the length of the string
let length = get_length(some_string);
println!("{:?}", length); // Output: Some(12)
// Using and_then to get the first character of the string
let first_char = get_first_char(some_string);
println!("{:?}", first_char); // Output: Some('H')
// Handling None case
let no_length = get_length(none_string);
println!("{:?}", no_length); // Output: None
let no_first_char = get_first_char(none_string);
println!("{:?}", no_first_char); // Output: None
}

<Bad Code>

// Function to get the length of a string if it exists


fn get_length(opt: Option<&str>) -> Option<usize> {
match opt {
Some(s) => Some(s.len()),
None => None,
}
}
// Function to get the first character of a string if it exists
fn get_first_char(opt: Option<&str>) -> Option<char> {
match opt {
Some(s) => s.chars().next(),
None => None,
}
}
fn main() {
let some_string = Some("Hello, Rust!");
let none_string: Option<&str> = None;
// Using match to get the length of the string
let length = get_length(some_string);
println!("{:?}", length); // Output: Some(12)
// Using match to get the first character of the string
let first_char = get_first_char(some_string);
println!("{:?}", first_char); // Output: Some('H')
// Handling None case
let no_length = get_length(none_string);
println!("{:?}", no_length); // Output: None
let no_first_char = get_first_char(none_string);
println!("{:?}", no_first_char); // Output: None
}
The good example uses map and and_then combinators to handle Option
values, making the code more concise and readable. The bad example uses
match statements, which are more verbose and can make the code harder to
read. Both examples achieve the same functionality, but the use of
combinators is generally preferred for readability and simplicity.

<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 >

// Function to parse a string into an integer, returning a Result


fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse::<i32>()
}
// Function to get the first element of a vector, returning an Option
fn get_first_element(vec: &Vec<i32>) -> Option<i32> {
vec.get(0).cloned()
}
fn main() {
let valid_number = "42";
let invalid_number = "abc";
// Handling Result from parse_number
match parse_number(valid_number) {
Ok(n) => println!("Parsed number: {}", n),
Err(e) => println!("Failed to parse number: {}", e),
}
match parse_number(invalid_number) {
Ok(n) => println!("Parsed number: {}", n),
Err(e) => println!("Failed to parse number: {}", e),
}
let numbers = vec![1, 2, 3];
let empty_vec: Vec<i32> = vec![];
// Handling Option from get_first_element
match get_first_element(&numbers) {
Some(n) => println!("First element: {}", n),
None => println!("No elements in the vector"),
}
match get_first_element(&empty_vec) {
Some(n) => println!("First element: {}", n),
None => println!("No elements in the vector"),
}
}

<Bad Code>

// Function to parse a string into an integer, returning a Result


fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse::<i32>()
}
// Function to get the first element of a vector, returning an Option
fn get_first_element(vec: &Vec<i32>) -> Option<i32> {
vec.get(0).cloned()
}
fn main() {
let valid_number = "42";
let invalid_number = "abc";
// Not handling Result from parse_number
let parsed_valid = parse_number(valid_number).unwrap();
println!("Parsed number: {}", parsed_valid);
// This will panic if the string is not a valid number
let parsed_invalid = parse_number(invalid_number).unwrap();
println!("Parsed number: {}", parsed_invalid);
let numbers = vec![1, 2, 3];
let empty_vec: Vec<i32> = vec![];
// Not handling Option from get_first_element
let first_element = get_first_element(&numbers).unwrap();
println!("First element: {}", first_element);
// This will panic if the vector is empty
let no_element = get_first_element(&empty_vec).unwrap();
println!("First element: {}", no_element);
}

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>

fn process_data(input: &str) -> String {


if input.contains("special") {
// If the input contains "special", we need to modify it, so we clone it.
input.replace("special", "ordinary")
} else {
// Otherwise, we clone the input unnecessarily.
input.to_string()
}
}
fn main() {
let data = "This is a special string.";
let result = process_data(data);
println!("{}", result); // Output: This is a ordinary string.
}

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.

Rc (Reference Counted) and Arc (Atomic Reference Counted) are used in


Rust to enable multiple ownership of data. Rc is used for single-threaded
scenarios, while Arc is used for multi-threaded scenarios.
< Good 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 = 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.

RefCell allows for mutable borrowing at runtime, while Mutex provides


mutual exclusion for safe access to data across threads.
< Good Code >

// Good example using RefCell and Mutex


use std::cell::RefCell;
use std::sync::{Arc, Mutex};
use std::thread;
// Using RefCell for interior mutability
struct MyStruct {
value: RefCell<i32>,
}
impl MyStruct {
fn new(val: i32) -> Self {
MyStruct {
value: RefCell::new(val),
}
}
fn update_value(&self, new_val: i32) {
*self.value.borrow_mut() = new_val;
}
}
// Using Mutex for thread safety
fn main() {
let data = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut num = data.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *data.lock().unwrap());
}

<Bad Code>

// Bad example using RefCell and Mutex


use std::cell::RefCell;
use std::sync::{Arc, Mutex};
use std::thread;
// Incorrect use of RefCell leading to runtime panic
struct MyStruct {
value: RefCell<i32>,
}
impl MyStruct {
fn new(val: i32) -> Self {
MyStruct {
value: RefCell::new(val),
}
}
fn update_value(&self, new_val: i32) {
let _borrow1 = self.value.borrow();
let _borrow2 = self.value.borrow_mut(); // This will panic at runtime
}
}
// Incorrect use of Mutex leading to deadlock
fn main() {
let data = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
let num = data.lock().unwrap();
*num += 1; // This will cause a deadlock
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *data.lock().unwrap());
}

In the good example, RefCell is used correctly to allow mutable access to


the value within MyStruct, and Mutex is used to ensure thread-safe access
to shared data. In the bad example, RefCell is misused by attempting to
borrow mutably while already borrowed immutably, causing a runtime
panic. The Mutex example shows a potential deadlock scenario by not
properly handling the lock.

<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.

In Rust, asynchronous programming can be achieved using the async and


await keywords. This approach helps in writing non-blocking code that is
easier to read and maintain.
< Good Code >

use tokio::time::{sleep, Duration};


// An asynchronous function that simulates a delay
async fn perform_task() {
println!("Task started");
sleep(Duration::from_secs(2)).await;
println!("Task completed");
}
#[tokio::main]
async fn main() {
println!("Starting main function");
perform_task().await;
println!("Main function completed");
}

<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.

In Rust, as in any programming language, using descriptive variable names


is crucial for code readability and maintainability. It allows other developers
(and your future self) to quickly grasp the purpose of each variable.
< Good Code >

fn calculate_area_of_rectangle(width: f64, height: f64) -> f64 {


width * height
}
fn main() {
let rectangle_width = 5.0;
let rectangle_height = 10.0;
let area = calculate_area_of_rectangle(rectangle_width,
rectangle_height);
println!("The area of the rectangle is: {}", area);
}

<Bad Code>

fn calc(w: f64, h: f64) -> f64 {


w*h
}
fn main() {
let w = 5.0;
let h = 10.0;
let a = calc(w, h);
println!("The area is: {}", a);
}

The good example uses descriptive names like rectangle_width and


rectangle_height, making it clear what the variables represent. The bad
example uses short, non-descriptive names like w, h, and a, which can be
confusing and require additional context to understand.

<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.

The following examples demonstrate the difference between using single-


letter variable names and descriptive names in a function that calculates the
area of a rectangle.
< Good Code >

// Function to calculate the area of a rectangle


fn calculate_rectangle_area(length: f64, width: f64) -> f64 {
// Calculate and return the area
length * width
}
fn main() {
let rectangle_length = 10.0;
let rectangle_width = 5.0;
let area = calculate_rectangle_area(rectangle_length, rectangle_width);
println!("The area of the rectangle is: {}", area);
}

<Bad Code>

// Function to calculate the area of a rectangle


fn c(l: f64, w: f64) -> f64 {
// Calculate and return the area
l*w
}
fn main() {
let x = 10.0;
let y = 5.0;
let a = c(x, y);
println!("The area of the rectangle is: {}", a);
}

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 >

fn sum_numbers(numbers: &Vec<i32>) -> i32 {


let mut total = 0; // Mutable because we'll change its value
for number in numbers {
total += number; // Modifying total, so it needs to be mutable
}
total // Return the final sum
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let result = sum_numbers(&numbers);
println!("The sum is: {}", result);
}

<Bad Code>

fn sum_numbers(mut numbers: Vec<i32>) -> i32 {


let mut total = 0;
let mut i = 0;
while i < numbers.len() {
total = total + numbers[i];
i = i + 1;
}
total
}
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
let mut result = sum_numbers(numbers);
println!("The sum is: {}", result);
}

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.

The following code demonstrates how grouping related variables can


enhance code clarity:
< Good Code >

// Good example: Related variables are grouped together


struct UserProfile {
// Personal information
name: String,
age: u32,
email: String,
// Account details
username: String,
password: String,
// Preferences
theme: String,
notifications_enabled: bool,
}
fn main() {
let user = UserProfile {
// Personal information
name: String::from("John Doe"),
age: 30,
email: String::from("[email protected]"),
// Account details
username: String::from("johndoe"),
password: String::from("securepass123"),
// Preferences
theme: String::from("dark"),
notifications_enabled: true,
};
// Use the user profile
println!("Welcome, {}!", user.name);
}

<Bad Code>

// Bad example: Related variables are scattered


struct UserProfile {
name: String,
username: String,
age: u32,
theme: String,
email: String,
notifications_enabled: bool,
password: String,
}
fn main() {
let user = UserProfile {
name: String::from("John Doe"),
username: String::from("johndoe"),
age: 30,
theme: String::from("dark"),
email: String::from("[email protected]"),
notifications_enabled: true,
password: String::from("securepass123"),
};
// Use the user profile
println!("Welcome, {}!", user.name);
}

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 >

// Good example: Using constants for unchanging values


const MAX_USERS: usize = 100;
const DEFAULT_TIMEOUT_MS: u64 = 5000;
fn process_users(users: &[String]) {
if users.len() > MAX_USERS {
println!("Error: Too many users. Maximum allowed: {}",
MAX_USERS);
return;
}
for user in users {
// Process user
println!("Processing user: {}", user);
}
}
fn main() {
let users = vec![
String::from("Alice"),
String::from("Bob"),
String::from("Charlie"),
];
process_users(&users);
println!("Default timeout: {} ms", DEFAULT_TIMEOUT_MS);
}
<Bad Code>

// Bad example: Using magic numbers instead of constants


fn process_users(users: &[String]) {
if users.len() > 100 {
println!("Error: Too many users. Maximum allowed: 100");
return;
}
for user in users {
// Process user
println!("Processing user: {}", user);
}
}
fn main() {
let users = vec![
String::from("Alice"),
String::from("Bob"),
String::from("Charlie"),
];
process_users(&users);
println!("Default timeout: 5000 ms");
}

The good example uses constants (MAX_USERS and


DEFAULT_TIMEOUT_MS) to define values that don't change. This
approach improves code readability and makes it easier to update these
values if needed. The bad example uses magic numbers directly in the code,
which can lead to confusion and make the code harder to maintain.

<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.

Breaking down complex functions into smaller, single-purpose functions


makes your code easier to read, test, and maintain. This approach adheres to
the Single Responsibility Principle, ensuring each function does one thing
well.
< Good Code >

// Function to calculate the area of a rectangle


fn calculate_area(length: f64, width: f64) -> f64 {
length * width
}
// Function to calculate the perimeter of a rectangle
fn calculate_perimeter(length: f64, width: f64) -> f64 {
2.0 * (length + width)
}
// Main function that uses the above helper functions
fn main() {
let length = 5.0;
let width = 3.0;
let area = calculate_area(length, width);
let perimeter = calculate_perimeter(length, width);
println!("Area: {}", area);
println!("Perimeter: {}", perimeter);
}
<Bad Code>

// Function that calculates both area and perimeter of a rectangle


fn calculate_area_and_perimeter(length: f64, width: f64) {
let area = length * width;
let perimeter = 2.0 * (length + width);
println!("Area: {}", area);
println!("Perimeter: {}", perimeter);
}
fn main() {
let length = 5.0;
let width = 3.0;
calculate_area_and_perimeter(length, width);
}

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.

Using helper functions to encapsulate repetitive code helps to avoid


redundancy, making your code cleaner and easier to maintain. This practice
also reduces the risk of errors and makes updates easier.
< Good Code >

// Helper function to print a greeting message


fn print_greeting(name: &str) {
println!("Hello, {}!", name);
}
// Main function that uses the helper function
fn main() {
let names = vec!["Alice", "Bob", "Charlie"];
for name in names {
print_greeting(name);
}
}

<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

The following examples demonstrate how to write a function that calculates


the sum of an array without side effects:
< Good Code >

// Good: Pure function without side effects


fn sum_array(numbers: &[i32]) -> i32 {
// Use fold to sum up the array elements
numbers.iter().fold(0, |acc, &x| acc + x)
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let result = sum_array(&numbers);
println!("Sum: {}", result);
}

<Bad Code>

// Bad: Function with side effects


static mut TOTAL: i32 = 0;
fn sum_array_with_side_effects(numbers: &[i32]) {
// Unsafe block to modify static mutable variable
unsafe {
for &num in numbers {
TOTAL += num;
}
}
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
sum_array_with_side_effects(&numbers);
// Unsafe block to read static mutable variable
unsafe {
println!("Sum: {}", TOTAL);
}
}

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 >

// Good: Clear and descriptive function name


fn calculate_average(numbers: &[f64]) -> Option<f64> {
if numbers.is_empty() {
None
} else {
let sum: f64 = numbers.iter().sum();
Some(sum / numbers.len() as f64)
}
}
fn main() {
let numbers = vec![1.0, 2.0, 3.0, 4.0, 5.0];
match calculate_average(&numbers) {
Some(avg) => println!("Average: {:.2}", avg),
None => println!("Cannot calculate average of an empty array"),
}
}

<Bad Code>

// Bad: Unclear and ambiguous function name


fn process(data: &[f64]) -> Option<f64> {
if data.is_empty() {
None
} else {
let s: f64 = data.iter().sum();
Some(s / data.len() as f64)
}
}
fn main() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
match process(&data) {
Some(result) => println!("Result: {:.2}", result),
None => println!("Error: Empty input"),
}
}

The good example uses a clear and descriptive function name


calculate_average, which immediately conveys its purpose. It also uses
meaningful parameter names like numbers.
The bad example uses a vague function name process and parameter name
data, which don't provide any information about what the function does.
This makes the code harder to understand and maintain.

<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 following code demonstrates how to refactor a function with many


parameters into a more manageable form.
< Good Code >

// Good: Using a struct to group related parameters


struct UserConfig {
name: String,
age: u32,
email: String,
preferences: Vec<String>,
}
fn create_user(config: UserConfig) {
// Function implementation
println!("Creating user: {} ({})", config.name, config.email);
}
fn main() {
let user_config = UserConfig {
name: String::from("Alice"),
age: 30,
email: String::from("[email protected]"),
preferences: vec![String::from("dark mode"),
String::from("notifications off")],
};
create_user(user_config);
}
<Bad Code>

// Bad: Function with too many parameters


fn create_user(name: String, age: u32, email: String, preference1: String,
preference2: String) {
// Function implementation
println!("Creating user: {} ({})", name, email);
}
fn main() {
create_user(
String::from("Alice"),
30,
String::from("[email protected]"),
String::from("dark mode"),
String::from("notifications off"),
);
}

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 >

// Good: Using impl blocks to organize methods


struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Constructor
fn new(width: u32, height: u32) -> Self {
Rectangle { width, height }
}
// Method to calculate area
fn area(&self) -> u32 {
self.width * self.height
}
// Method to check if it's a square
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let rect = Rectangle::new(10, 20);
println!("Area: {}", rect.area());
println!("Is square? {}", rect.is_square());
}

<Bad Code>

// Bad: Methods defined separately without impl blocks


struct Rectangle {
width: u32,
height: u32,
}
fn new_rectangle(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
fn area(rect: &Rectangle) -> u32 {
rect.width * rect.height
}
fn is_square(rect: &Rectangle) -> bool {
rect.width == rect.height
}
fn main() {
let rect = new_rectangle(10, 20);
println!("Area: {}", area(&rect));
println!("Is square? {}", is_square(&rect));
}

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 >

// Define a custom struct


struct Person {
name: String,
age: u32,
}
// Implement From trait for converting a tuple to Person
impl From<(String, u32)> for Person {
fn from(tuple: (String, u32)) -> Self {
Person {
name: tuple.0,
age: tuple.1,
}
}
}
fn main() {
// Use the From trait to convert a tuple to Person
let person: Person = ("Alice".to_string(), 30).into();
println!("Name: {}, Age: {}", person.name, person.age);
}
<Bad 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>

fn process_string(input: &str) -> String {


if input.contains("rust") {
input.to_string()
} else {
input.to_uppercase()
}
}
fn main() {
let s1 = "I love rust programming";
let s2 = "I love coding";
println!("Processed: {}", process_string(s1));
println!("Processed: {}", process_string(s2));
}

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.

The following code demonstrates proper use of Rc for shared ownership in


a single-threaded context:
< Good Code >

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
}

The good example uses Rc to create shared ownership of the SharedData


instance. This allows multiple references to the same data without copying
it. The reference count is automatically managed, and the data will be
cleaned up when all references are dropped. The bad example attempts to
manually manage shared references, which can lead to ownership and
lifetime issues, especially in more complex scenarios.

<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.

The following code demonstrates how to break down a large expression


into smaller, more manageable parts:
< Good Code >

// Good example: Breaking down a complex expression


fn calculate_total_cost(price: f64, quantity: i32, tax_rate: f64, discount: f64)
-> f64 {
// Break down the calculation into smaller steps
let subtotal = price * quantity as f64;
let tax_amount = subtotal * tax_rate;
let discount_amount = subtotal * discount;
// Calculate the final total
let total = subtotal + tax_amount - discount_amount;
total
}

<Bad Code>

// Bad example: Complex expression in a single line


fn calculate_total_cost(price: f64, quantity: i32, tax_rate: f64, discount: f64)
-> f64 {
// All calculations in one line, making it hard to read and understand
price * quantity as f64 * (1.0 + tax_rate - discount)
}
The good example breaks down the complex calculation into smaller,
named variables. This approach makes the code more readable and easier to
understand. Each step of the calculation is clear, and it's easier to spot
potential errors or make modifications.
The bad example combines all calculations into a single line, making it
difficult to follow the logic and potentially introducing errors that are hard
to detect.

<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 >

// Good example: Using intermediate variables


fn process_data(data: Vec<i32>) -> (i32, i32, f64) {
let sum: i32 = data.iter().sum();
let count = data.len() as i32;
// Store intermediate results in named variables
let average = if count > 0 { sum as f64 / count as f64 } else { 0.0 };
let max_value = *data.iter().max().unwrap_or(&0);
(sum, max_value, average)
}

<Bad Code>

// Bad example: Not using intermediate variables


fn process_data(data: Vec<i32>) -> (i32, i32, f64) {
(
data.iter().sum(),
*data.iter().max().unwrap_or(&0),
if data.len() > 0 { data.iter().sum::<i32>() as f64 / data.len() as f64 }
else { 0.0 }
)
}
The good example uses intermediate variables to store the results of sub-
expressions. This approach makes the code more readable and self-
documenting. Each variable has a clear purpose, and the logic is easier to
follow.
The bad example combines all operations into a single tuple return
statement. This makes the code more compact but harder to read and
understand. It's also more prone to errors and harder to debug or modify.

<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.

The following code demonstrates refactoring a complex expression into a


helper function:
< Good Code >

// Helper function to calculate discount


fn calculate_discount(price: f64, quantity: u32) -> f64 {
let base_discount = if quantity > 10 { 0.1 } else { 0.05 };
let bulk_discount = (quantity as f64 * 0.01).min(0.2);
price * (1.0 - base_discount - bulk_discount)
}
fn main() {
let price = 100.0;
let quantity = 15;
// Use the helper function
let final_price = calculate_discount(price, quantity);
println!("Final price: ${:.2}", final_price);
}

<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);
}

The good code example introduces a helper function calculate_discount that


encapsulates the complex discount calculation logic. This approach
improves readability by giving the calculation a descriptive name and
separating it from the main function. The bad code example performs the
same calculation inline, making it harder to understand at a glance.

<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.

When writing Rust code, it's important to maintain readability. Chaining


multiple method calls in a single line can obscure the logic and make it
harder for others to understand and maintain the code.
< Good Code >

// 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>

// Bad example: Too many method calls chained in a single line.


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]

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.

In Rust, breaking down complex functions into smaller, well-defined


functions helps in managing the code better. It makes the code more
modular, easier to test, and enhances readability.
< Good Code >

// Good example: Sub-problems are extracted into separate functions.


fn main() {
let data = vec![1, 2, 3, 4, 5];
let filtered_data = filter_data(&data);
let transformed_data = transform_data(&filtered_data);
println!("{:?}", transformed_data); // Output: [6, 8, 10]
}
fn filter_data(data: &Vec<i32>) -> Vec<i32> {
data.iter().filter(|&&x| x > 2).cloned().collect()
}
fn transform_data(data: &Vec<i32>) -> Vec<i32> {
data.iter().map(|&x| x * 2).collect()
}

<Bad Code>

// Bad example: All logic is contained within the main function.


fn main() {
let data = vec![1, 2, 3, 4, 5];
let transformed_data: Vec<i32> = data.iter()
.filter(|&&x| x > 2)
.map(|&x| x * 2)
.collect();
println!("{:?}", transformed_data); // Output: [6, 8, 10]
}

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 >

// Function to calculate the area of a rectangle


fn calculate_area(width: f64, height: f64) -> f64 {
width * height
}
// Function to print the area of a rectangle
fn print_area(width: f64, height: f64) {
let area = calculate_area(width, height);
println!("The area of the rectangle is: {}", area);
}
fn main() {
let width = 5.0;
let height = 10.0;
print_area(width, height);
}

<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.

Modularizing your code helps in organizing it logically, making it easier to


navigate and understand. It also promotes reusability and maintainability.
< Good Code >

// module for geometry-related functions


mod geometry {
pub fn calculate_area(width: f64, height: f64) -> f64 {
width * height
}
}
// module for printing-related functions
mod printer {
pub fn print_area(area: f64) {
println!("The area of the rectangle is: {}", area);
}
}
fn main() {
let width = 5.0;
let height = 10.0;
let area = geometry::calculate_area(width, height);
printer::print_area(area);
}

<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 >

// Define a trait with shared behavior


trait Describable {
fn describe(&self) -> String;
}
// Implement the trait for a struct
struct Dog {
name: String,
age: u8,
}
impl Describable for Dog {
fn describe(&self) -> String {
format!("Dog named {} is {} years old.", self.name, self.age)
}
}
// Implement the trait for another struct
struct Cat {
name: String,
color: String,
}
impl Describable for Cat {
fn describe(&self) -> String {
format!("Cat named {} is {} in color.", self.name, self.color)
}
}
// Function that works with any Describable type
fn print_description(item: &impl Describable) {
println!("{}", item.describe());
}
fn main() {
let dog = Dog { name: String::from("Buddy"), age: 3 };
let cat = Cat { name: String::from("Whiskers"), color:
String::from("black") };
print_description(&dog);
print_description(&cat);
}

<Bad Code>

// No trait used, leading to code duplication


struct Dog {
name: String,
age: u8,
}
impl Dog {
fn describe(&self) -> String {
format!("Dog named {} is {} years old.", self.name, self.age)
}
}
struct Cat {
name: String,
color: String,
}
impl Cat {
fn describe(&self) -> String {
format!("Cat named {} is {} in color.", self.name, self.color)
}
}
fn print_dog_description(dog: &Dog) {
println!("{}", dog.describe());
}
fn print_cat_description(cat: &Cat) {
println!("{}", cat.describe());
}
fn main() {
let dog = Dog { name: String::from("Buddy"), age: 3 };
let cat = Cat { name: String::from("Whiskers"), color:
String::from("black") };
print_dog_description(&dog);
print_cat_description(&cat);
}

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.

Refactoring large functions into smaller ones helps in isolating


functionality, making the code easier to understand, test, and maintain. It
also promotes reusability of code.
< Good Code >

// Function broken down into smaller pieces


fn calculate_area(length: f64, width: f64) -> f64 {
length * width
}
fn calculate_perimeter(length: f64, width: f64) -> f64 {
2.0 * (length + width)
}
fn print_rectangle_properties(length: f64, width: f64) {
let area = calculate_area(length, width);
let perimeter = calculate_perimeter(length, width);
println!("Area: {}", area);
println!("Perimeter: {}", perimeter);
}
fn main() {
let length = 5.0;
let width = 3.0;
print_rectangle_properties(length, width);
}
<Bad Code>

// Large function with multiple responsibilities


fn print_rectangle_properties(length: f64, width: f64) {
let area = length * width;
let perimeter = 2.0 * (length + width);
println!("Area: {}", area);
println!("Perimeter: {}", perimeter);
}
fn main() {
let length = 5.0;
let width = 3.0;
print_rectangle_properties(length, width);
}

In the good example, the large function print_rectangle_properties is


refactored into smaller functions calculate_area and calculate_perimeter.
This makes the code more modular and easier to understand. Each function
has a single responsibility, which improves readability and maintainability.
In the bad example, the function print_rectangle_properties handles
multiple responsibilities, 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 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.

The ? operator in Rust is a shorthand for propagating errors. It reduces


boilerplate code and makes functions more readable by automatically
converting errors into the appropriate type.
< 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 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
}
}

In the good example, the ? operator is used to handle errors concisely. If an


error occurs, it is automatically returned from the function. This reduces the
need for explicit match statements and makes the code cleaner and easier to
read. In the bad example, errors are manually propagated using match
statements, which adds unnecessary verbosity and complexity.

<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 >

// Define a trait for polymorphism


trait Shape {
fn area(&self) -> f64;
}
// Implement the trait for a struct
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
// Function that takes a Box<dyn Shape>
fn print_area(shape: Box<dyn Shape>) {
println!("The area is {}", shape.area());
}
fn main() {
// Create a Circle and box it
let circle = Box::new(Circle { radius: 5.0 });
// Pass the boxed Circle to the function
print_area(circle);
}

<Bad Code>

// Define a trait for polymorphism


trait Shape {
fn area(&self) -> f64;
}
// Implement the trait for a struct
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
// Function that takes a reference to a trait object
fn print_area(shape: &Shape) {
println!("The area is {}", shape.area());
}
fn main() {
// Create a Circle
let circle = Circle { radius: 5.0 };
// Pass a reference to the Circle to the function
print_area(&circle);
}

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 >

// Function to find the first word in a string slice


fn first_word(s: &str) -> &str {
// Convert the string slice to bytes
let bytes = s.as_bytes();
// Iterate over the byte array
for (i, &item) in bytes.iter().enumerate() {
// Check for the space character
if item == b' ' {
// Return the slice up to the space
return &s[0..i];
}
}
// If no space is found, return the whole string slice
&s[..]
}
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string);
println!("The first word is: {}", word);
}

<Bad Code>

// Function to find the first word in a string


fn first_word(s: &String) -> &str {
// Convert the string to bytes
let bytes = s.as_bytes();
// Iterate over the byte array
for (i, &item) in bytes.iter().enumerate() {
// Check for the space character
if item == b' ' {
// Return the slice up to the space
return &s[0..i];
}
}
// If no space is found, return the whole string
&s[..]
}
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string);
println!("The first word is: {}", word);
}

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.

In Rust, BTreeMap is a collection that stores key-value pairs in a sorted


order based on the keys. It is useful when you need to maintain the order of
elements or perform range queries.
< Good 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
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 >

// Function to divide two numbers, returning a Result


fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
// Return an error if the divisor is zero
Err(String::from("Cannot divide by zero"))
} else {
// Return the result of the division
Ok(a / b)
}
}
fn main() {
// Attempt to divide 4.0 by 2.0
match divide(4.0, 2.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
// Attempt to divide 4.0 by 0.0
match divide(4.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
}

<Bad Code>

// Function to divide two numbers, returning a float


fn divide(a: f64, b: f64) -> f64 {
// No error handling for division by zero
a/b
}
fn main() {
// Attempt to divide 4.0 by 2.0
let result = divide(4.0, 2.0);
println!("Result: {}", result);
// Attempt to divide 4.0 by 0.0
// This will cause a runtime error
let result = divide(4.0, 0.0);
println!("Result: {}", result);
}

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.

In Rust, Rc (Reference Counted) and Arc (Atomic Reference Counted) are


smart pointers that enable multiple ownership of a value. Rc is used in
single-threaded scenarios, while Arc is thread-safe and can be used in multi-
threaded contexts.
< Good Code >

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();
}

In the good example, Rc is used for single-threaded reference counting, and


Arc is used for multi-threaded reference counting, ensuring thread safety.
The bad example incorrectly uses Rc in a multi-threaded context, which is
not thread-safe and will result in a compile-time error.

<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);
}

The good example uses Result to handle potential errors gracefully. It


defines a custom error type FileReadError that encapsulates both IO errors
and a specific error for empty files. The read_file_contents function returns
a Result<String, FileReadError>, allowing the caller to handle different
error cases.
The bad example, on the other hand, uses unwrap() which will panic if an
error occurs. This approach is less robust and doesn't provide the caller with
any way to handle errors gracefully.

<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.

The following code demonstrates the use of unwrap_or and unwrap_or_else


in a function that parses a configuration value:
< Good Code >

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);
}

The good example uses unwrap_or_else to provide default values and


handle potential errors gracefully. In get_config_value, if the environment
variable is not set, it returns a default value with a warning message.
Similarly, parse_timeout uses unwrap_or_else to provide a default timeout
value if parsing fails.
The bad example uses panic! when a configuration value is not set and
unwrap() when parsing the timeout value. This approach is less flexible and
can cause the program to crash if the expected values are not present or in
the correct format.

<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 >

// Good example: Using `expect` with a meaningful error message


fn main() {
let num_str = "42";
let num: i32 = num_str.parse().expect("Failed to parse 'num_str' as i32");
println!("The number is: {}", num);
}

<Bad Code>

// Bad example: Using `expect` without a meaningful error message


fn main() {
let num_str = "42";
let num: i32 = num_str.parse().expect("Error");
println!("The number is: {}", num);
}

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 >

// Good example: Handling errors at the appropriate level


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)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
fn main() {
match read_file_content("example.txt") {
Ok(content) => println!("File content: {}", content),
Err(e) => eprintln!("Failed to read file: {}", e),
}
}

<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);
}

In the good example, the read_file_content function returns a Result,


allowing the caller to handle the error appropriately. This makes the
function more flexible and reusable. In the bad example, the function panics
on error, which is not suitable for production code as it does not allow the
caller to handle the error gracefully.

<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 >

// Importing necessary crates


use thiserror::Error;
use anyhow::{Result, Context};
// Define a custom error type using thiserror
#[derive(Error, Debug)]
pub enum MyError {
#[error("An error occurred: {0}")]
CustomError(String),
#[error("Another error occurred")]
AnotherError,
}
// A function that returns a Result with custom error
fn do_something(value: i32) -> Result<(), MyError> {
if value < 0 {
Err(MyError::CustomError("Value must be non-negative".to_string()))
} else {
Ok(())
}
}
// Using anyhow for error handling in main function
fn main() -> Result<()> {
do_something(-1).context("Failed to do something")?;
Ok(())
}

<Bad Code>

// A function that returns a Result with a generic error


fn do_something(value: i32) -> Result<(), String> {
if value < 0 {
Err("Value must be non-negative".to_string())
} else {
Ok(())
}
}
// Using generic error handling in main function
fn main() -> Result<(), String> {
do_something(-1)?;
Ok(())
}

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 >

// A generic function to find the largest element in a slice


fn largest<T: PartialOrd>(list: &[T]) -> &T {
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(&number_list));
println!("The largest char is {}", largest(&char_list));
}

<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));
}

In the good example, a single generic function largest is defined to handle


slices of any type that implements the PartialOrd trait. This makes the code
more concise and reusable. In the bad example, separate functions are
written for i32 and char types, leading to code duplication and reduced
maintainability.

<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.

The following code demonstrates the use of traits to define a common


interface for different shapes:
< Good Code >

// Define a trait for shapes


trait Shape {
fn area(&self) -> f64;
}
// Implement the Shape trait for Circle
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
// Implement the Shape trait for Rectangle
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
// Function that works with any type implementing the Shape trait
fn print_area<T: Shape>(shape: &T) {
println!("Area: {}", shape.area());
}
fn main() {
let circle = Circle { radius: 5.0 };
let rectangle = Rectangle { width: 4.0, height: 3.0 };
print_area(&circle);
print_area(&rectangle);
}

<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.

The following code demonstrates the use of impl Trait in function


signatures:
< Good Code >

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 following example demonstrates a macro that simplifies logging with


different severity levels.
< Good Code >

// Define a macro for logging with different severity levels


macro_rules! log {
($level:expr, $($arg:tt)+) => {{
let level_str = match $level {
0 => "DEBUG",
1 => "INFO",
2 => "WARN",
3 => "ERROR",
_ => "UNKNOWN",
};
println!("[{}] {}", level_str, format!($($arg)+));
}};
}
fn main() {
// Using the macro for different log levels
log!(0, "Debug message: {}", "This is a debug log");
log!(1, "Info message: {}", "This is an info log");
log!(2, "Warning message: {}", "This is a warning log");
log!(3, "Error message: {}", "This is an error log");
}
<Bad Code>

// Repetitive and hard-to-maintain logging code


fn main() {
println!("[DEBUG] Debug message: {}", "This is a debug log");
println!("[INFO] Info message: {}", "This is an info log");
println!("[WARN] Warning message: {}", "This is a warning log");
println!("[ERROR] Error message: {}", "This is an error log");
}

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.

This example shows how to refactor a repeated pattern of reading and


processing files into a reusable function.
< Good Code >

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.

Cargo workspaces allow you to manage multiple related packages in a


single repository, making it easier to share code, manage dependencies, and
build the entire project.
< Good Code >

toml# Cargo.toml at the root of the workspace


[workspace]
members = [
"package1",
"package2",
]
# package1/Cargo.toml
[package]
name = "package1"
version = "0.1.0"
edition = "2021"
[dependencies]
package2 = { path = "../package2" }
# package2/Cargo.toml
[package]
name = "package2"
version = "0.1.0"
edition = "2021"
<Bad Code>

toml# Cargo.toml at the root of the workspace


[workspace]
members = [
"package1",
"package2",
]
# package1/Cargo.toml
[package]
name = "package1"
version = "0.1.0"
edition = "2021"
[dependencies]
# Missing dependency on package2
# package2/Cargo.toml
[package]
name = "package2"
version = "0.1.0"
edition = "2021"

In the good example, package1 correctly specifies a dependency on


package2 using a relative path, ensuring that the packages can share code
and be built together. In the bad example, package1 does not specify its
dependency on package2, which can lead to build errors and difficulties in
managing shared code.

<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.

Throughout the chapters, we explored various methods to enhance code


readability, supported by clear examples of both good and bad practices.
The goal was to provide you with a comprehensive toolkit to write cleaner,
more efficient Rust code.

As an author, your feedback is invaluable to me.


I read every review and take your comments to heart.
Your insights help shape future works and ensure that the content remains
relevant and useful.
Whether you found the book helpful, engaging, or if it fell short of your
expectations, I would love to hear your thoughts.

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.

Thank you once again for your time and support.


I look forward to the possibility of connecting with you through future
publications.

You might also like