100.Techniques.for.Writing.readable.code.in.csharp.B0D5TBQ63F
100.Techniques.for.Writing.readable.code.in.csharp.B0D5TBQ63F
Use var for local variable declarations when the type is obvious.
Utilize using statements to manage resource cleanup.
Leverage async and await for asynchronous programming.
Use LINQ for querying collections in a readable manner.
Implement properties instead of public fields for encapsulation.
Name variables with meaningful and descriptive names.
Use PascalCase for class names and method names
Use camelCase for local variables and method parameters
Prefix boolean variables with "is" or "has" to indicate their type.
Avoid abbreviations in names unless they are widely understood.
Avoid using single-letter variable names except in loops.
Use specific names instead of generic ones like data or info.
Avoid using magic numbers; use named constants instead
Ensure method names clearly describe their functionality
Use namespaces to avoid name collisions and organize code.
Use StringBuilder for concatenating large strings.
Utilize null-coalescing operator ?? to provide default values.
Use nameof operator for argument validation.
Leverage pattern matching for cleaner type checks
Use expression-bodied members for simple methods and properties
Comment on complex logic to explain the reasoning.
Document the purpose of public methods and classes.
Use TODO comments to indicate areas for future improvement.
Comment on any workarounds or hacks in the code.
Explain the Intent Behind Non-Obvious Code
Keep Comments Up-to-Date with Code Changes
Avoid redundant comments that state the obvious.
Use XML documentation comments for public APIs.
Write comments in complete sentences for clarity.
Use comments to explain why, not what.
Use readonly for fields that should not be modified after initialization
Leverage auto-properties for simple property declarations
Use tuples for returning multiple values from a method.
Utilize extension methods to add functionality to existing types.
Use delegates and events for implementing the observer pattern.
Keep lines of code within a reasonable length.
Use consistent indentation and formatting.
Group related code together logically.
Use whitespace to separate logical sections of code.
Align similar code vertically for better readability.
Use if-else statements instead of nested ternary operators.
Avoid deep nesting by using guard clauses.
Use switch statements for multiple conditions.
Break long methods into smaller, more manageable ones.
Use foreach instead of for when iterating over collections.
Use try-catch blocks to handle exceptions gracefully.
Avoid empty catch blocks; at least log the exception.
Use finally blocks to clean up resources.
Throw specific exceptions instead of generic ones.
Use using statements to ensure proper disposal of resources.
Use const for values that never change.
Declare variables as close to their usage as possible.
Use meaningful names for all variables
Avoid using the same variable for multiple purposes
Use var when the type is clear from the context.
Limit the scope of variables to the smallest possible.
Use descriptive names for loop counters.
Avoid global variables; use class fields or properties instead.
Use readonly for variables that should not change after initialization.
Initialize variables where they are declared.
Ensure each method performs a single, well-defined task.
Avoid long methods; break them into smaller ones.
Use helper methods to encapsulate complex logic.
Refactor code to eliminate duplication.
Use meaningful method names that describe their purpose.
Use async and await for asynchronous operations.
Leverage LINQ for querying and manipulating collections.
Use yield return for implementing custom iterators.
Utilize Task and Task<T> for parallel programming.
Use lock statements to handle concurrency issues.
Break down complex expressions into multiple lines.
Use intermediate variables to store results of sub-expressions.
Use parentheses to make the order of operations clear.
Avoid chaining multiple method calls in a single statement.
Use descriptive variable names for intermediate results.
Extract unrelated logic into separate methods.
Use helper methods to encapsulate common functionality.
Refactor large methods to improve readability.
Use design patterns to solve common problems.
Encapsulate complex logic within well-named methods.
Use async and await for asynchronous programming.
Leverage LINQ for querying collections.
Use using statements for resource management.
Utilize pattern matching for cleaner code.
Use expression-bodied members for simple methods.
Choose the appropriate collection type for your data.
Use Dictionary for key-value pairs.
Use List for ordered collections.
Use HashSet for unique elements.
Use Queue and Stack for FIFO and LIFO collections.
Use try-catch blocks to handle exceptions.
Log exceptions for debugging purposes.
Use custom exceptions for specific error conditions.
Validate method arguments to prevent errors.
Use finally blocks to clean up resources.
Write reusable methods for common tasks.
Use interfaces to define contracts for classes.
Leverage inheritance to reuse code.
Use generics to create flexible and reusable code.
Encapsulate common functionality in utility classes.
Use dependency injection to manage dependencies.
Write unit tests to ensure code correctness.
Introduction
◆
Each technique is designed to help you write code that is not only
functional but also clear and maintainable.
Included are examples of both good and bad coding practices, structured in
a way that makes it easy to grasp the principles being discussed.
Using 'var' in local variable declarations helps make the code cleaner and
easier to read, especially when the variable type is evident from the right-
hand side of the assignment.
< Good Code >
<Bad Code>
In the good example, using 'var' makes the code shorter and easier to read
without sacrificing clarity. The type of the variable is still clear from the
context, which improves maintainability. In the bad example, the explicit
type declaration adds unnecessary verbosity, making the code harder to read
without adding value.
<Memo>
C# 3.0 introduced 'var' as part of the language's type inference capabilities,
which allows the compiler to deduce the type of a variable from its
initializer.
2
Utilize using statements to manage
resource cleanup.
Ensure proper resource management and cleanup by using 'using'
statements to handle IDisposable objects.
<Bad Code>
In the good example, the 'using' statement ensures that the SqlConnection
object is disposed of as soon as the block of code is exited, even if an
exception occurs. This guarantees that resources are properly freed. In the
bad example, manual disposal is prone to errors and can lead to resource
leaks if an exception prevents the Dispose method from being called.
<Memo>
The 'using' statement was introduced in C# 1.0 to simplify resource
management by ensuring deterministic disposal of unmanaged resources.
3
Leverage async and await for
asynchronous programming.
Using async and await keywords in C# to write asynchronous code that is
easy to read and maintain.
The async and await keywords in C# allow you to write asynchronous code
that looks similar to synchronous code, making it easier to read and
understand.
< Good Code >
<Bad Code>
In the good example, the await keyword is used to asynchronously wait for
the GetStringAsync method to complete, without blocking the thread. This
makes the code more readable and efficient. In the bad example, the Wait
method is used, which blocks the thread and negates the benefits of
asynchronous programming.
<Memo>
The async keyword was introduced in C# 5.0, along with the await
keyword, to simplify asynchronous programming and improve code
readability.
4
Use LINQ for querying collections in a
readable manner.
Utilize LINQ (Language Integrated Query) to perform queries on
collections in a concise and readable way.
LINQ provides a powerful syntax for querying collections, making the code
more readable and expressive compared to traditional loops and
conditionals.
< Good Code >
<Bad Code>
<Memo>
LINQ was introduced in C# 3.0 and provides a consistent model for
working with data across various sources, such as collections, databases,
and XML.
5
Implement properties instead of public
fields for encapsulation.
Use properties to encapsulate fields and provide controlled access.
<Bad Code>
csharppublic class Person
{
// Public field without encapsulation
public string name;
}
In the good example, the Name property encapsulates the name field,
allowing for validation logic to be added when setting the value. This
ensures that the name cannot be set to a null or empty string. In the bad
example, the name field is public, meaning it can be directly accessed and
modified without any control, potentially leading to invalid states.
<Memo>
Properties in C# can also be auto-implemented, which simplifies the syntax
when no additional logic is needed in the getter or setter.
6
Name variables with meaningful and
descriptive names.
Choose variable names that clearly describe their purpose and use.
Meaningful and descriptive variable names make code more readable and
maintainable. They help other developers understand the purpose of the
variable without needing additional comments or documentation.
< Good Code >
<Bad Code>
<Memo>
Using meaningful variable names is part of writing self-documenting code,
which reduces the need for extensive comments and makes the codebase
easier to navigate.
7
Use PascalCase for class names and
method names
Use PascalCase naming convention for classes and methods in C#. This
improves code readability and follows the standard C# coding style.
<Bad Code>
<Memo>
PascalCase is also known as UpperCamelCase.
8
Use camelCase for local variables and
method parameters
Use camelCase naming convention for local variables and method
parameters in C#. This improves code readability and follows the standard
C# coding style.
<Bad Code>
<Memo>
camelCase is also known as lowerCamelCase.
9
Prefix boolean variables with "is" or
"has" to indicate their type.
Use "is" or "has" as prefixes for boolean variables to make their purpose
clear.
<Bad Code>
In the good code example, the prefixes "is" and "has" clearly indicate that
the variables are boolean, making the code more readable and
understandable. In the bad code example, the lack of these prefixes makes it
less obvious that the variables are boolean, potentially causing confusion.
<Memo>
The practice of prefixing boolean variables with "is" or "has" is common in
many programming languages, not just C#. It helps maintain consistency
and readability across different codebases.
10
Avoid abbreviations in names unless they
are widely understood.
Use full words for variable and method names unless the abbreviation is
universally recognized.
Abbreviations can make code harder to read and understand. Using full
words ensures clarity, except when the abbreviation is well-known and
widely accepted.
< Good Code >
<Bad Code>
In the good code example, using full words like "userAge" and
"customerAddress" makes the code self-explanatory. In the bad code
example, abbreviations like "usrAg" and "custAddr" can confuse readers
who may not immediately understand what the variables represent.
<Memo>
Some widely accepted abbreviations, like "ID" for "identifier" or "URL" for
"Uniform Resource Locator," are exceptions to this rule because they are
universally understood in the programming community.
11
Avoid using single-letter variable names
except in loops.
Use meaningful variable names to enhance code readability.
In C#, using single-letter variable names outside of loops can make code
hard to understand. Instead, use descriptive names that convey the purpose
of the variable.
< Good Code >
<Bad Code>
In the good example, length, width, and area clearly describe their purpose,
making the code easier to read and understand. In the bad example, l, w, and
a are ambiguous and require more effort to interpret.
<Memo>
Single-letter variable names are traditionally used in mathematics and are
often seen in loop counters (e.g., i, j, k). However, for clarity, avoid their
use elsewhere in your code.
12
Use specific names instead of generic
ones like data or info.
Choose specific, descriptive names for variables to make code more
understandable.
Generic variable names like data or info provide little context about what
the variable represents. Using specific names helps others quickly grasp the
variable's role and content.
< Good Code >
<Bad Code>
<Memo>
Using specific names also helps with code maintenance and debugging, as
it reduces the likelihood of confusion and errors, making the development
process more efficient.
13
Avoid using magic numbers; use named
constants instead
Instead of using literal values (magic numbers) in your code, define named
constants to improve code readability and maintainability.
When working with numeric values in code, it's often better to use named
constants instead of literal values (magic numbers). This makes the code
more self-documenting and easier to understand and maintain.
< Good Code >
<Bad Code>
<Memo>
The term "magic number" refers to the use of literal values in code without
any explanation or context, making it difficult for others (or even yourself
in the future) to understand their meaning or purpose.
14
Ensure method names clearly describe
their functionality
Choose descriptive and self-documenting method names that accurately
convey the purpose and behavior of the method.
<Bad Code>
<Memo>
The principle of using clear and descriptive names is often referred to as the
"Self-Documenting Code" principle, which emphasizes writing code that is
easy to understand and maintain without relying heavily on external
documentation or comments.
15
Use namespaces to avoid name collisions
and organize code.
Namespaces help prevent name collisions and keep your code organized.
<Bad Code>
<Memo>
The concept of namespaces is not unique to C#. Many other programming
languages like Java, C++, and Python also use namespaces or similar
constructs to organize code and avoid name conflicts.
16
Use StringBuilder for concatenating
large strings.
StringBuilder is more efficient than using string concatenation for handling
large or numerous string operations.
<Bad Code>
// Bad example using string concatenation
public class Example
{
public string BuildLargeString()
{
string result = "";
for (int i = 0; i < 10000; i++)
{
result += "This is line " + i + "\n";
}
return result;
}
}
<Memo>
StringBuilder is part of the System.Text namespace in .NET. It is
specifically designed for scenarios where frequent modifications to string
contents are required, making it a crucial tool for performance optimization
in string manipulation tasks.
17
Utilize null-coalescing operator ?? to
provide default values.
Use the null-coalescing operator (??) to assign default values to variables
that might be null.
<Bad Code>
<Memo>
The null-coalescing operator (??) was introduced in C# 2.0 and has since
been a valuable tool for developers to manage null values efficiently.
18
Use nameof operator for argument
validation.
Use the nameof operator to improve argument validation and exception
handling, making the code clearer and less error-prone.
<Bad Code>
The good example uses the nameof operator, which helps avoid hardcoding
parameter names and reduces the risk of errors during refactoring. The bad
example hardcodes the parameter name, making it less maintainable and
prone to mistakes if the parameter name changes.
<Memo>
The nameof operator was introduced in C# 6.0, providing a more reliable
way to refer to variable and parameter names in exception handling and
logging.
19
Leverage pattern matching for cleaner
type checks
Pattern matching simplifies complex type checks and improves code
readability.
Pattern matching allows you to check the type of an object and perform
different actions based on the type, all in a single expression.
< Good Code >
<Bad Code>
The good code example uses pattern matching to check the type of the
shape object and perform the appropriate area calculation in a single
expression. This approach is more concise and easier to read than the
traditional if-else statements used in the bad code example.
<Memo>
Pattern matching was introduced in C# 7.0 and has been further enhanced in
subsequent versions.
20
Use expression-bodied members for
simple methods and properties
Expression-bodied members provide a concise syntax for simple methods
and properties.
<Bad Code>
<Memo>
Expression-bodied members were introduced in C# 6.0 and are particularly
useful for implementing simple properties and methods.
21
Comment on complex logic to explain
the reasoning.
Use comments to clarify complex logic in your code.
<Bad Code>
In the good code example, comments are used to explain the base case and
the recursive case, making it clear why the code is written in this way. In
the bad code example, the lack of comments makes it harder for others to
understand the logic, especially if they are not familiar with recursion.
<Memo>
Comments should not state the obvious but should provide insight into the
reasoning behind complex or non-intuitive code.
22
Document the purpose of public methods
and classes.
Always document the purpose of public methods and classes.
Public methods and classes should have clear documentation that describes
their purpose and usage. This helps other developers understand how to use
them correctly and what to expect from them.
< Good Code >
csharp/// <summary>
/// Represents a customer in the system.
/// </summary>
public class Customer
{
/// <summary>
/// Gets or sets the customer's name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Calculates the discount for the customer based on their purchase
history.
/// </summary>
/// <returns>The discount percentage.</returns>
public double CalculateDiscount()
{
// Logic to calculate discount
return 0.0;
}
}
<Bad Code>
<Memo>
XML documentation comments can be used to generate API documentation
automatically, making it easier to maintain and share documentation with
other developers.
23
Use TODO comments to indicate areas
for future improvement.
Use TODO comments to mark sections of code that need further work or
enhancements.
TODO comments help developers identify parts of the code that require
additional attention or improvements in the future.
< Good Code >
<Bad Code>
In the good example, the TODO comment clearly indicates that the method
should be optimized in the future. This helps other developers quickly
understand that there is a known area for improvement. In the bad example,
the comment is vague and does not provide a clear action item.
<Memo>
TODO comments can be easily searched and tracked using IDEs, making it
simpler to manage and address them over time.
24
Comment on any workarounds or hacks
in the code.
Always comment on any workarounds or hacks to explain why they are
necessary.
<Bad Code>
In the good example, the comment explains that the hardcoded user ID is a
temporary hack for demonstration purposes and indicates that it should be
replaced with a proper database call. This provides clarity and context for
future developers. In the bad example, the lack of comments leaves other
developers guessing about the purpose and necessity of the hardcoded
value.
<Memo>
Documenting workarounds and hacks can prevent potential bugs and
misunderstandings, ensuring that temporary solutions are revisited and
properly addressed in the future.
25
Explain the Intent Behind Non-Obvious
Code
Clearly document the purpose and logic behind complex or non-intuitive
code sections.
When working with intricate algorithms, data structures, or legacy code, it's
essential to provide explanations for sections that may not be immediately
understandable.
< Good Code >
<Bad Code>
The good code example clearly explains that the method implements the
Quicksort algorithm for sorting an array in ascending order. The comments
provide context for the recursive calls and their purpose. In contrast, the bad
code example lacks any explanations, making it difficult to understand the
algorithm's intent or the logic behind the code.
<Memo>
Commenting non-obvious code not only aids in understanding but also
facilitates future maintenance and collaboration.
26
Keep Comments Up-to-Date with Code
Changes
Ensure that comments accurately reflect the current state of the code by
updating them whenever changes are made.
<Bad Code>
<Memo>
Outdated comments can be more harmful than no comments at all, as they
can mislead developers and introduce bugs or misunderstandings.
27
Avoid redundant comments that state the
obvious.
Write comments that add value and avoid stating what the code already
clearly expresses.
Comments should provide insight into the code's purpose or complex logic,
not restate what is evident from the code itself.
< Good Code >
<Bad Code>
In the good example, the comment explains the purpose of the code
(calculating the area of a circle) and provides a useful formula. In the bad
example, the comments merely restate what the code is doing, which is
already clear from the code itself.
<Memo>
Redundant comments can clutter the code and make it harder to maintain.
Focus on writing self-explanatory code and use comments to explain the
"why" rather than the "what."
28
Use XML documentation comments for
public APIs.
Utilize XML documentation comments to provide detailed information
about public methods, properties, and classes.
csharp/// <summary>
/// Calculates the area of a circle.
/// </summary>
/// <param name="radius">The radius of the circle.</param>
/// <returns>The area of the circle.</returns>
public double CalculateCircleArea(double radius)
{
return Math.PI * Math.Pow(radius, 2);
}
<Bad Code>
<Memo>
XML documentation comments can be processed by tools like Sandcastle
or Doxygen to generate detailed API documentation, which is especially
useful for large projects and libraries.
29
Write comments in complete sentences
for clarity.
Using complete sentences in comments helps ensure clarity and readability
for all developers.
<Bad Code>
// factorial calculation
public int CalculateFactorial(int number)
{
if (number <= 1)
{
return 1; // base case
}
else
{
return number * CalculateFactorial(number - 1); // recursion
}
}
In the good example, the comments provide complete sentences that clearly
explain the purpose and logic of the code. In the bad example, the
comments are brief and lack detail, making it harder to understand the
code's intent.Complete sentences in comments ensure that the code's
functionality and logic are communicated effectively, reducing the
likelihood of misunderstandings and errors.
<Memo>
Using complete sentences in comments can also aid non-native English
speakers by providing full context, making it easier to understand and
translate the code explanations.
30
Use comments to explain why, not what.
Comments should focus on explaining the reasoning behind the code rather
than describing what the code does.
<Bad Code>
// create dictionary
Dictionary<string, User> users = new Dictionary<string, User>();
// add user
users.Add("john_doe", new User("John", "Doe"));
In the good example, the comments explain why a dictionary is used and
why the username must be unique. This provides context and rationale for
the code's structure. In the bad example, the comments merely describe the
actions being taken, which is redundant because the code itself is self-
explanatory.By explaining the reasons behind the code, comments become
more informative and valuable, helping developers understand the design
choices and making future modifications easier.
<Memo>
Explaining the "why" in comments can also document design patterns or
specific algorithms used, providing insight into the overall architecture and
making the codebase more robust and maintainable.
31
Use readonly for fields that should not be
modified after initialization
Use the readonly keyword to prevent fields from being modified after
initialization, improving code clarity and preventing accidental mutations.
<Bad Code>
In the good code example, _radius and _pi are marked as readonly, ensuring
their values cannot be modified after initialization. This prevents accidental
mutations and makes the code more readable and maintainable. In the bad
code example, _pi can be accidentally modified, leading to incorrect results.
<Memo>
The readonly keyword is a compile-time constraint, meaning the compiler
enforces the rule. It does not prevent reflection or unsafe code from
modifying the field.
32
Leverage auto-properties for simple
property declarations
Use auto-properties for simple property declarations to reduce boilerplate
code and improve code readability.
<Bad Code>
In the good code example, auto-properties are used, reducing the amount of
code needed for simple property declarations. The bad code example shows
the traditional way of declaring properties, which requires more code and
can be harder to read and maintain.
<Memo>
Auto-properties were introduced in C# 3.0 and have become a widely
adopted practice for simple property declarations. They can also be
combined with other features like data annotations or property initializers.
33
Use tuples for returning multiple values
from a method.
A simple technique to enhance code readability by using tuples to return
multiple values from a method in C#.
Using tuples allows methods to return more than one value without the need
for creating custom classes or out parameters, making the code cleaner and
easier to read.
< Good Code >
<Bad Code>
public void Calculate(int a, int b, out int sum, out int product)
{
sum = a + b;
product = a * b;
}
public void Example()
{
int sum, product;
Calculate(3, 4, out sum, out product);
Console.WriteLine($"Sum: {sum}, Product: {product}"); // Uses out
parameters
}
Using tuples makes the code more concise and easier to understand by
avoiding out parameters, which can be confusing and error-prone. Tuples
provide named elements, enhancing readability.
<Memo>
Tuples were introduced in C# 7.0 and provide a lightweight way to group
multiple values. They are commonly used for short-lived data structures
that do not require a separate class.
34
Utilize extension methods to add
functionality to existing types.
Extension methods allow you to add new methods to existing types without
modifying their source code or creating derived types.
<Memo>
Extension methods were introduced in C# 3.0. They are static methods but
can be called as if they were instance methods on the extended type,
providing syntactic sugar for more intuitive code.
35
Use delegates and events for
implementing the observer pattern.
Delegates and events in C# provide a robust way to implement the observer
pattern, allowing objects to notify other objects about changes.
csharpusing System;
public class Subject
{
// Declare the delegate (if using non-generic pattern).
public delegate void Notify();
// Declare the event using the delegate.
public event Notify OnNotify;
public void ChangeState()
{
Console.WriteLine("State has changed.");
// Notify all observers about the state change.
OnNotify?.Invoke();
}
}
public class Observer
{
public void Subscribe(Subject subject)
{
subject.OnNotify += Update;
}
private void Update()
{
Console.WriteLine("Observer has been notified of the state change.");
}
}
public class Program
{
public static void Main()
{
Subject subject = new Subject();
Observer observer = new Observer();
observer.Subscribe(subject);
subject.ChangeState();
}
}
<Bad Code>
csharpusing System;
public class Subject
{
public Action OnNotify; // Using Action instead of a proper delegate
public void ChangeState()
{
Console.WriteLine("State has changed.");
if (OnNotify != null)
{
OnNotify(); // No null-conditional operator
}
}
}
public class Observer
{
public void Subscribe(Subject subject)
{
subject.OnNotify += Update;
}
private void Update()
{
Console.WriteLine("Observer has been notified of the state change.");
}
}
public class Program
{
public static void Main()
{
Subject subject = new Subject();
Observer observer = new Observer();
observer.Subscribe(subject);
subject.ChangeState();
}
}
In the good example, the delegate and event are explicitly declared, making
the code more readable and maintainable. The null-conditional operator (?.)
is used to safely invoke the event. In the bad example, an Action delegate is
used, which is less descriptive, and the null check is done manually, which
is more error-prone.
<Memo>
The observer pattern is widely used in implementing distributed event-
handling systems, such as in the Model-View-Controller (MVC)
architectural pattern.
36
Keep lines of code within a reasonable
length.
Maintaining a reasonable line length in your code improves readability and
helps prevent horizontal scrolling.
Long lines of code can be difficult to read and maintain. Keeping lines
within a reasonable length (typically 80-100 characters) ensures that the
code is more readable and easier to work with, especially in environments
with limited screen space.
< Good Code >
<Bad Code>
In the good example, the code is broken down into multiple lines, making it
easier to read and understand. Each logical step is on its own line, which
improves clarity. In the bad example, all the code is crammed into a single
line, making it hard to read and understand.
<Memo>
The practice of keeping lines of code short dates back to the early days of
programming when screens and printouts had limited width. This practice
continues to be relevant for improving code readability and maintainability.
37
Use consistent indentation and
formatting.
Consistent indentation and formatting make code easier to read and
maintain.
<Bad Code>
<Memo>
Most modern IDEs and code editors have features to automatically format
code according to a set of style guidelines, which can help maintain
consistency.
38
Group related code together logically.
Grouping related code together improves readability and maintainability.
<Bad Code>
<Memo>
Using regions in C# can help in organizing code by grouping related
sections together, which can be collapsed or expanded in the IDE for better
readability.
39
Use whitespace to separate logical
sections of code.
Using whitespace effectively can make your code more readable by clearly
separating different logical sections.
<Bad Code>
csharppublic class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Subtract(int a, int b)
{
return a - b;
}
public int Multiply(int a, int b)
{
return a * b;
}
}
<Memo>
Whitespace in code does not affect the execution of the program but
significantly impacts readability and maintainability.
40
Align similar code vertically for better
readability.
Aligning similar lines of code vertically can make patterns and structures in
your code more apparent.
<Bad Code>
<Memo>
Vertical alignment is particularly useful in data structures and configuration
files where similar elements are defined repeatedly.
41
Use if-else statements instead of nested
ternary operators.
Simplify code readability by avoiding complex nested ternary operators and
using if-else statements.
When writing C# code, using nested ternary operators can make the code
difficult to read and understand. Using if-else statements improves clarity
and maintainability.
< Good Code >
<Bad Code>
// Bad Example: Using nested ternary operators
int value = 10;
string result = value > 0 ? "Positive" : value < 0 ? "Negative" : "Zero";
Console.WriteLine(result); // Output: Positive
Using if-else statements, as shown in the good example, makes the logic
clear and easy to follow. Each condition is explicitly separated, which helps
in understanding the code flow. On the other hand, the nested ternary
operator in the bad example, while concise, is harder to read and
understand, especially for those not familiar with the syntax or for more
complex conditions.
<Memo>
The ternary operator is also known as the conditional operator and is the
only operator in C# that takes three operands. While it can be useful for
simple conditions, it should be used sparingly to maintain code readability.
42
Avoid deep nesting by using guard
clauses.
Improve code readability and reduce complexity by using guard clauses to
handle edge cases early.
<Bad Code>
Using guard clauses, as shown in the good example, allows you to handle
error conditions early and exit the method if necessary. This keeps the main
logic at the main level of the method, making it easier to read and maintain.
In contrast, the bad example demonstrates deep nesting, which makes the
code harder to follow and increases cognitive load.
<Memo>
Guard clauses help to adhere to the "Fail Fast" principle, which suggests
that a program should immediately report any condition that is likely to
indicate a failure. This helps in identifying issues early and improves
overall code quality.
43
Use switch statements for multiple
conditions.
Utilize switch statements to handle multiple conditions efficiently,
improving readability and maintainability.
<Bad Code>
<Memo>
The switch statement in C# can also be used with string types starting from
C# 7.0, adding flexibility in handling multiple conditions.
44
Break long methods into smaller, more
manageable ones.
Divide long methods into smaller, focused methods to enhance readability
and maintainability.
<Bad Code>
<Memo>
Following the Single Responsibility Principle (SRP) from SOLID
principles helps in creating methods that focus on one task, improving code
quality and maintainability.
45
Use foreach instead of for when iterating
over collections.
The foreach loop simplifies iteration over collections, making code more
readable and less error-prone.
<Bad Code>
The foreach loop automatically handles the iteration over each element in
the collection without needing to manage the index manually. This makes
the code less prone to off-by-one errors and easier to read. The for loop,
while more flexible, requires manual handling of the index and can
introduce bugs if not managed correctly.
<Memo>
The foreach loop in C# is syntactic sugar for using the enumerator pattern,
which internally uses the IEnumerator interface to iterate over a collection.
46
Use try-catch blocks to handle
exceptions gracefully.
Try-catch blocks allow you to handle exceptions gracefully, preventing the
application from crashing and providing meaningful error messages.
Using try-catch blocks around code that can throw exceptions ensures that
your application can handle errors gracefully. In the good example, specific
exceptions are caught, and appropriate messages are displayed. The bad
example lacks error handling, which can lead to unhandled exceptions and
application crashes, leaving the user without guidance on what went wrong.
<Memo>
In C#, it's good practice to catch specific exceptions before catching the
general Exception type to handle known error conditions appropriately and
avoid masking unexpected issues.
47
Avoid empty catch blocks; at least log
the exception.
Empty catch blocks can hide errors and make debugging difficult. Always
log exceptions to understand what went wrong.
Empty catch blocks are a common mistake that can lead to silent failures.
By logging exceptions, you ensure that errors are recorded and can be
addressed.
< Good Code >
csharptry
{
// Code that may throw an exception
}
catch (Exception ex)
{
// Log the exception
Console.WriteLine($"An error occurred: {ex.Message}");
}
<Bad Code>
csharptry
{
// Code that may throw an exception
}
catch (Exception)
{
// Empty catch block
}
<Memo>
Logging frameworks like NLog or log4net can be used to log exceptions to
various outputs, such as files, databases, or remote servers, providing more
flexibility and control over logging.
48
Use finally blocks to clean up resources.
Finally blocks ensure that resources are released properly, even if an
exception occurs.
Finally blocks are used to execute code that must run regardless of whether
an exception is thrown, such as closing files or releasing network resources.
< Good Code >
<Bad Code>
csharpFileStream fileStream = null;
try
{
fileStream = new FileStream("example.txt", FileMode.Open);
// Code that works with the file
}
catch (Exception ex)
{
// Log the exception
Console.WriteLine($"An error occurred: {ex.Message}");
}
// No finally block to ensure the file is closed
In the good example, the finally block ensures that the file stream is closed,
preventing resource leaks. In the bad example, if an exception occurs, the
file stream may not be closed, leading to potential resource leaks.
<Memo>
The using statement in C# can be used as a shorthand for try-finally blocks
when working with objects that implement the IDisposable interface,
ensuring that resources are automatically cleaned up.
49
Throw specific exceptions instead of
generic ones.
Using specific exceptions makes error handling more precise and easier to
debug.
<Bad Code>
<Memo>
Using specific exceptions can improve maintainability and debugging by
allowing developers to catch and handle different types of errors
appropriately.
50
Use using statements to ensure proper
disposal of resources.
The using statement ensures that disposable resources are properly released.
Using the using statement helps manage the lifecycle of resources that
implement IDisposable, ensuring they are correctly disposed of even if an
exception occurs.
< Good Code >
<Bad Code>
In the good example, the using statement ensures that the StreamReader is
disposed of automatically when it goes out of scope. In the bad example,
the disposal is done manually in a finally block, which is more error-prone
and verbose.
<Memo>
The using statement in C# is syntactic sugar that simplifies the usage of the
try-finally pattern for disposing of resources.
51
Use const for values that never change.
Declare constant values using the const keyword to improve code
readability and maintainability.
Using const for values that never change ensures that these values are easily
identifiable and prevents accidental modification.
< Good Code >
<Bad Code>
<Memo>
In C#, const fields are implicitly static and must be initialized with a
constant expression at the time of declaration. They cannot be changed
thereafter.
52
Declare variables as close to their usage
as possible.
Define variables at the point of first use to enhance code clarity and reduce
the scope of the variables.
<Bad Code>
In the good example, the variable result is declared right where it is needed,
making the code more readable and its scope limited to the loop. In the bad
example, result is declared at the beginning of the method, which can cause
confusion and potential misuse as its purpose is not immediately clear.
<Memo>
Reducing the scope of variables by declaring them close to their usage can
help in avoiding errors related to variable reuse and makes the code easier
to understand for others.
53
Use meaningful names for all variables
Give variables descriptive names that reflect their purpose and content.
When declaring variables, choose names that clearly convey their meaning
and role in the code.
< Good Code >
<Bad Code>
Using meaningful names makes the code more readable and easier to
understand, especially for other developers or when revisiting the code after
some time. It helps convey the intent and purpose of each variable, reducing
the cognitive load required to comprehend the code.
<Memo>
Many coding style guides, such as Microsoft's .NET Framework Design
Guidelines, recommend using descriptive and meaningful names for all
identifiers, including variables.
54
Avoid using the same variable for
multiple purposes
Assign a single responsibility to each variable and avoid reusing it for
different purposes.
Variables should have a clear and consistent role throughout their scope.
Reusing the same variable for multiple purposes can lead to confusion and
potential bugs.
< Good Code >
<Bad Code>
Reusing variables for different purposes can make the code harder to read,
understand, and maintain. It can also introduce subtle bugs if the variable's
value is unintentionally overwritten or used in an unexpected context. By
assigning a single responsibility to each variable, the code becomes more
self-documenting and less prone to errors.
<Memo>
The principle of "single responsibility" is a fundamental concept in software
design and is often applied not only to variables but also to classes,
functions, and other code constructs.
55
Use var when the type is clear from the
context.
Using var can make your code cleaner and more readable when the type is
obvious from the context.
When the type of a variable is clear from the right-hand side of the
assignment, using var can reduce redundancy and improve readability.
< Good Code >
csharpvar customer = new Customer(); // The type is clear from the context
var totalAmount = 100.0; // The type is clear from the context
<Bad Code>
In the good example, var is used because the type of the variable is evident
from the right-hand side of the assignment. This reduces redundancy and
makes the code cleaner. In the bad example, the type is explicitly declared,
which is unnecessary and makes the code more verbose.
<Memo>
The var keyword was introduced in C# 3.0 as part of the language's support
for implicitly typed local variables.
56
Limit the scope of variables to the
smallest possible.
Declare variables in the smallest scope possible to improve readability and
maintainability.
<Bad Code>
In the good example, the discount variable is declared within the if block,
limiting its scope to where it is needed. This makes the code easier to read
and reduces the risk of errors. In the bad example, discount is declared at
the beginning of the method, giving it a broader scope than necessary,
which can lead to potential misuse or errors.
<Memo>
Limiting the scope of variables is a principle of good software design
known as "encapsulation," which helps in managing complexity and
improving code quality.
57
Use descriptive names for loop counters.
Using descriptive names for loop counters improves code readability and
maintainability.
When writing loops, using generic names like i, j, or k can make the code
harder to understand. Instead, use descriptive names that indicate the
purpose of the loop counter.
< Good Code >
<Bad Code>
In the good example, studentIndex clearly indicates that the loop counter is
used to index students. This makes the code more understandable at a
glance. In the bad example, i is less informative, requiring the reader to
infer its purpose from the context.
<Memo>
Descriptive names are not only useful for loop counters but also for
variables, methods, and classes. They help in making the code self-
documenting, reducing the need for excessive comments.
58
Avoid global variables; use class fields or
properties instead.
Using class fields or properties instead of global variables enhances
encapsulation and reduces potential side effects.
Global variables can be accessed and modified from anywhere in the code,
making it difficult to track changes and debug issues. Using class fields or
properties confines the scope of variables, improving code structure and
reliability.
< Good Code >
In the good example, the name field is encapsulated within the Student
class, and access is controlled through the Name property. This approach
ensures that the variable is only modified in a controlled manner. In the bad
example, the global variable name can be modified from anywhere, leading
to potential bugs and making the code harder to maintain.
<Memo>
Encapsulation is a fundamental principle of object-oriented programming
(OOP). It helps in bundling the data with the methods that operate on the
data, restricting direct access to some of the object's components.
59
Use readonly for variables that should
not change after initialization.
Using readonly ensures that variables are only assigned once, improving
code reliability and readability.
<Bad Code>
<Memo>
The readonly keyword in C# is similar to the final keyword in Java and the
const keyword in C++, but readonly allows for initialization in the
constructor, providing more flexibility.
60
Initialize variables where they are
declared.
Initializing variables at the point of declaration improves code clarity and
reduces the risk of uninitialized variables.
Initializing variables where they are declared helps ensure that they are
always in a valid state and makes the code easier to read and maintain.
< Good Code >
<Bad Code>
<Memo>
Initializing variables at the point of declaration is a common practice in
many programming languages, including C#, Java, and Python, as it
promotes better coding habits and reduces the likelihood of runtime errors.
61
Ensure each method performs a single,
well-defined task.
Write methods that focus on a single responsibility to improve readability
and maintainability.
A method should do one thing and do it well. This makes the code easier to
understand, test, and maintain.
< Good Code >
<Bad Code>
In the good example, the IsUserValid method only checks if the user is
valid, adhering to the single responsibility principle. In the bad example, the
method also logs the validation, which should be a separate concern.
<Memo>
The Single Responsibility Principle (SRP) is one of the SOLID principles
of object-oriented design, which states that a class or method should have
only one reason to change.
62
Avoid long methods; break them into
smaller ones.
Refactor long methods into smaller, more manageable ones to enhance
readability and maintainability.
Long methods can be difficult to read and understand. Breaking them into
smaller methods makes the code more modular and easier to manage.
< Good Code >
<Bad Code>
In the good example, the ProcessOrder method is broken down into smaller
methods, each handling a specific task. This makes the code easier to read
and maintain. In the bad example, the ProcessOrder method is long and
handles multiple tasks, making it harder to understand and maintain.
<Memo>
Refactoring long methods into smaller ones is a common practice in clean
code principles, which helps in achieving better code modularity and
reusability.
63
Use helper methods to encapsulate
complex logic.
Encapsulate complex logic into helper methods to improve code readability
and maintainability.
Breaking down complex logic into smaller, reusable helper methods makes
the code easier to understand and maintain.
< Good Code >
<Bad Code>
In the good example, the complex logic is encapsulated into helper methods
(IsOrderValid, CalculateOrderTotal, and SaveOrder), making the
ProcessOrder method cleaner and easier to read. In the bad example, all the
logic is crammed into the ProcessOrder method, making it harder to
understand and maintain.
<Memo>
Encapsulating logic into helper methods not only improves readability but
also promotes code reuse and easier unit testing.
64
Refactor code to eliminate duplication.
Refactor your code to remove duplication and improve maintainability.
<Bad Code>
<Memo>
Refactoring to eliminate duplication is a core principle of the DRY (Don't
Repeat Yourself) principle, which aims to reduce repetition of code patterns
and improve overall code quality.
65
Use meaningful method names that
describe their purpose.
Choose method names that clearly describe what the method does.
<Bad Code>
<Memo>
Meaningful method names are part of the broader concept of self-
documenting code, which aims to make code more readable and
maintainable without relying heavily on external documentation.
66
Use async and await for asynchronous
operations.
Utilize async and await keywords to handle asynchronous operations in a
clear and efficient manner.
Using async and await makes asynchronous code easier to read and
maintain by avoiding complex callback patterns and improving error
handling.
< Good Code >
csharp// Good example: Using async and await for asynchronous operations
public async Task<string> FetchDataAsync(string url)
{
using (HttpClient client = new HttpClient())
{
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
<Bad Code>
In the good example, the async and await keywords are used to handle
asynchronous operations in a straightforward manner, making the code
easier to read and maintain. In the bad example, callbacks are used, which
can lead to more complex and harder-to-read code, often referred to as
"callback hell."
<Memo>
The async and await keywords were introduced in C# 5.0 to simplify
asynchronous programming and improve code readability by allowing
developers to write asynchronous code that looks similar to synchronous
code.
67
Leverage LINQ for querying and
manipulating collections.
Use LINQ to simplify querying and manipulating collections in a readable
and efficient manner.
LINQ (Language Integrated Query) allows for concise and readable code
when dealing with collections, making it easier to perform operations like
filtering, sorting, and projecting data.
< Good Code >
<Bad Code>
Using LINQ, the good example achieves filtering and sorting in a single,
readable line of code. In contrast, the bad example requires multiple steps,
making it more error-prone and harder to read. LINQ enhances code
readability and maintainability.
<Memo>
LINQ stands for Language Integrated Query and was introduced in .NET
Framework 3.5. It provides a consistent model for working with data across
various sources and formats.
68
Use yield return for implementing
custom iterators.
Use yield return to simplify the creation of custom iterators for collections.
The yield return statement allows you to define custom iteration logic
without the need to create a temporary collection or manage an explicit
state machine.
< Good Code >
<Bad Code>
The good example uses yield return to produce even numbers on-the-fly
without storing them in a temporary list, making the code more efficient
and easier to read. The bad example creates a temporary list to store results,
which is less efficient and more cumbersome.
<Memo>
The yield keyword in C# was introduced in C# 2.0, allowing developers to
implement stateful iterators in a straightforward manner without needing to
maintain complex state manually.
69
Utilize Task and Task<T> for parallel
programming.
Use Task and Task<T> to perform parallel programming efficiently in C#.
Task and Task<T> are part of the Task Parallel Library (TPL) in C#. They
provide a simple way to run code asynchronously and handle parallel
operations.
< Good Code >
csharpusing System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Task<int> task1 = Task.Run(() => Compute(10));
Task<int> task2 = Task.Run(() => Compute(20));
int result1 = await task1;
int result2 = await task2;
Console.WriteLine($"Result1: {result1}, Result2: {result2}");
}
static int Compute(int value)
{
// Simulate a time-consuming operation
Task.Delay(1000).Wait();
return value * value;
}
}
<Bad Code>
csharpusing System;
class Program
{
static void Main(string[] args)
{
int result1 = Compute(10);
int result2 = Compute(20);
Console.WriteLine($"Result1: {result1}, Result2: {result2}");
}
static int Compute(int value)
{
// Simulate a time-consuming operation
System.Threading.Thread.Sleep(1000);
return value * value;
}
}
<Memo>
The Task Parallel Library (TPL) was introduced in .NET Framework 4.0 to
simplify parallel programming and improve performance by utilizing
multiple cores of the CPU.
70
Use lock statements to handle
concurrency issues.
Use lock statements to manage access to shared resources and prevent race
conditions in multithreaded applications.
csharpusing System;
using System.Threading;
class Program
{
private static readonly object _lock = new object();
private static int _counter = 0;
static void Main(string[] args)
{
Thread thread1 = new Thread(IncrementCounter);
Thread thread2 = new Thread(IncrementCounter);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Final Counter: {_counter}");
}
static void IncrementCounter()
{
for (int i = 0; i < 1000; i++)
{
lock (_lock)
{
_counter++;
}
}
}
}
<Bad Code>
csharpusing System;
using System.Threading;
class Program
{
private static int _counter = 0;
static void Main(string[] args)
{
Thread thread1 = new Thread(IncrementCounter);
Thread thread2 = new Thread(IncrementCounter);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Final Counter: {_counter}");
}
static void IncrementCounter()
{
for (int i = 0; i < 1000; i++)
{
_counter++;
}
}
}
In the good example, the lock statement ensures that only one thread can
increment the _counter variable at a time, preventing race conditions. In the
bad example, the lack of synchronization allows multiple threads to
increment _counter simultaneously, leading to unpredictable results and
potential data corruption.
<Memo>
The lock statement is syntactic sugar for Monitor.Enter and Monitor.Exit,
providing a simpler way to ensure mutual exclusion in critical sections of
code.
71
Break down complex expressions into
multiple lines.
Splitting complex expressions into multiple lines enhances readability and
maintainability.
<Bad Code>
<Memo>
The concept of breaking down complex expressions into simpler steps is
often referred to as "stepwise refinement," which is a core principle in
computer programming to improve clarity and maintainability.
72
Use intermediate variables to store
results of sub-expressions.
Using intermediate variables helps clarify the purpose of each part of an
expression.
<Bad Code>
<Memo>
Using intermediate variables is a common practice in clean coding and is
recommended by many coding standards and guidelines, including those by
organizations like Microsoft and Google.
73
Use parentheses to make the order of
operations clear.
Using parentheses in expressions ensures that the order of operations is
explicit and clear to anyone reading the code.
In the good example, the use of parentheses makes it clear that the addition
and subtraction should be performed before the multiplication. In the bad
example, the order of operations is ambiguous and could lead to
misunderstandings or errors.
<Memo>
The order of operations in C# follows the standard mathematical
precedence: parentheses, exponents, multiplication and division, and finally
addition and subtraction.
74
Avoid chaining multiple method calls in
a single statement.
Chaining multiple method calls in a single statement can make the code
difficult to read and debug. It's better to break them into separate
statements.
While chaining method calls can make the code look concise, it often
sacrifices readability and makes debugging more challenging. Breaking
down the calls into separate statements improves clarity.
< Good Code >
<Bad Code>
In the good example, each step of the process is broken down into a
separate statement, making it clear what each step does. In the bad example,
chaining all the method calls together makes it harder to understand and
debug.
<Memo>
Method chaining is a common practice in fluent interfaces, but it should be
used judiciously to maintain code readability and simplicity.
75
Use descriptive variable names for
intermediate results.
Using descriptive variable names makes the code more readable and
maintainable.
When writing code, it's important to use variable names that clearly
describe their purpose, especially for intermediate results. This helps other
developers understand the code quickly.
< Good Code >
<Bad Code>
In the good example, the variable area clearly indicates what the value
represents, making the code easier to understand. In the bad example, the
variable c is not descriptive, which can confuse readers about its purpose.
<Memo>
Descriptive variable names are part of the broader concept of self-
documenting code, which aims to make code understandable without
extensive comments.
76
Extract unrelated logic into separate
methods.
Separating unrelated logic into different methods improves code readability
and reusability.
By extracting unrelated logic into separate methods, you can make your
code more modular and easier to understand. This practice also facilitates
testing and maintenance.
< Good Code >
<Bad Code>
In the good example, the logic for getting, processing, and saving data is
separated into different methods, making the code more modular and easier
to follow. In the bad example, all logic is combined in a single method,
making it harder to read and maintain.
<Memo>
Modular code is a key principle of the SOLID design principles,
particularly the Single Responsibility Principle, which states that a class or
method should have only one reason to change.
77
Use helper methods to encapsulate
common functionality.
Encapsulate repetitive code into helper methods to improve readability and
maintainability.
When you find yourself writing the same code multiple times, it's a good
practice to encapsulate that code into a helper method. This makes your
code more modular, easier to read, and simpler to maintain.
< Good Code >
<Bad Code>
csharp// Bad example: Repeating the same code in multiple methods
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Subtract(int a, int b)
{
return a - b;
}
}
<Memo>
Using helper methods not only improves code readability but also makes it
easier to test individual pieces of functionality, leading to more robust and
reliable code.
78
Refactor large methods to improve
readability.
Break down large methods into smaller, more manageable pieces to
enhance readability and maintainability.
<Bad Code>
<Memo>
The Single Responsibility Principle (SRP) is one of the SOLID principles
of object-oriented design. It states that a class or method should have only
one reason to change, which is achieved by refactoring large methods into
smaller ones with distinct responsibilities.
79
Use design patterns to solve common
problems.
Design patterns provide reusable solutions to common problems in software
design.
Using design patterns can make your code more modular, easier to
understand, and maintain. Here is an example using the Singleton pattern.
< Good Code >
<Bad Code>
The good example uses the Singleton pattern to ensure that only one
instance of the class is created, which is useful for managing shared
resources. The bad example creates multiple instances, which can lead to
resource conflicts and is harder to manage.
<Memo>
The Singleton pattern is one of the simplest design patterns and is often
used for logging, configuration settings, and managing connection pools.
80
Encapsulate complex logic within well-
named methods.
Encapsulating complex logic in well-named methods improves code
readability and maintainability.
<Bad Code>
The good example breaks down the order processing logic into smaller,
well-named methods, making it easier to understand and maintain. The bad
example has all the logic in a single method, making it harder to read and
modify.
<Memo>
Encapsulation is a fundamental principle of object-oriented programming
that helps in hiding the internal state and requiring all interaction to be
performed through an object's methods.
81
Use async and await for asynchronous
programming.
Simplify asynchronous programming by using async and await keywords.
Using async and await in C# makes asynchronous code easier to read and
maintain by avoiding callback hell and making the code look more like
synchronous code.
< Good Code >
<Bad Code>
The good example uses async and await to handle asynchronous operations,
making the code more readable and easier to understand. The bad example
uses ContinueWith, which can lead to more complex and harder-to-read
code, especially when handling exceptions.
<Memo>
The async and await keywords were introduced in C# 5.0 to simplify
asynchronous programming and improve code readability.
82
Leverage LINQ for querying collections.
Use LINQ to perform complex queries on collections in a readable and
concise manner.
<Bad Code>
The good example uses LINQ to filter the list of numbers, making the code
more concise and easier to read. The bad example uses a traditional loop to
achieve the same result, which is more verbose and less expressive.
<Memo>
LINQ was introduced in C# 3.0 and provides a consistent model for
working with data across various sources, such as collections, databases,
and XML.
83
Use using statements for resource
management.
Ensure proper disposal of resources by using using statements.
<Bad Code>
In the good example, the using statement ensures that the FileStream is
disposed of correctly, even if an exception occurs. In the bad example, the
FileStream is not disposed of, which can lead to resource leaks and other
issues.
<Memo>
The using statement can be used with any object that implements the
IDisposable interface, making it a versatile tool for resource management in
C#.
84
Utilize pattern matching for cleaner code.
Use pattern matching to simplify conditional logic and improve code
readability.
<Bad Code>
In the good example, pattern matching is used to both check the type of obj
and declare a new variable s in a single, concise statement. In the bad
example, the type check and casting are done separately, making the code
more verbose and harder to read.
<Memo>
Pattern matching was introduced in C# 7.0 and has been expanded in
subsequent versions, providing more powerful and expressive ways to
handle type checks and value extraction.
85
Use expression-bodied members for
simple methods.
Simplify your code by using expression-bodied members for methods that
contain a single expression.
<Bad Code>
csharp// Bad example using a full method body for a simple operation
public class Calculator
{
// Multi-line method for a simple addition
public int Add(int a, int b)
{
return a + b;
}
}
The good example uses an expression-bodied member to make the code
more concise and readable. The bad example uses a full method body,
which is unnecessary for such a simple operation. Expression-bodied
members improve code readability and reduce boilerplate code.
<Memo>
Expression-bodied members were introduced in C# 6.0 and have been
expanded in later versions to include constructors, destructors, and property
accessors.
86
Choose the appropriate collection type
for your data.
Select the most suitable collection type based on the characteristics and
requirements of your data.
Using the right collection type can improve the performance, readability,
and maintainability of your code. Consider factors like data size, access
patterns, and required operations when choosing a collection.
< Good Code >
<Bad Code>
The good example uses a List<string>, which is more suitable for dynamic
data as it automatically resizes and provides convenient methods for adding
and accessing elements. The bad example uses an array, which requires
manual resizing and additional logic to handle dynamic data, making the
code more complex and error-prone.
<Memo>
The .NET framework provides various collection types such as List<T>,
Dictionary<TKey, TValue>, HashSet<T>, and Queue<T>, each optimized
for different scenarios and operations.
87
Use Dictionary for key-value pairs.
Dictionaries provide an efficient way to store and retrieve data using unique
keys.
<Bad Code>
The good example uses a Dictionary, which provides O(1) average time
complexity for lookups, making it efficient and easy to read. The bad
example uses parallel arrays, which require O(n) time complexity for
lookups and are harder to maintain and understand.
<Memo>
Dictionaries in C# are implemented using hash tables, which allow for fast
data retrieval based on keys.
88
Use List for ordered collections.
Lists provide a flexible way to store ordered collections of items.
<Bad Code>
The good example uses a List, which is more flexible and easier to work
with than arrays. Lists can dynamically resize, and they provide many
useful methods for manipulating the collection. The bad example uses a
fixed-size array, which is less flexible and requires manual resizing if the
number of elements changes.
<Memo>
Lists in C# are part of the System.Collections.Generic namespace and
provide many useful methods such as Add, Remove, and Contains.
89
Use HashSet for unique elements.
HashSet ensures that all elements are unique and provides efficient
operations for adding, removing, and checking for elements.
csharpusing System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a HashSet to store unique elements
HashSet<int> uniqueNumbers = new HashSet<int>();
// Add elements to the HashSet
uniqueNumbers.Add(1);
uniqueNumbers.Add(2);
uniqueNumbers.Add(3);
uniqueNumbers.Add(1); // Duplicate, will not be added
// Display the elements
foreach (int number in uniqueNumbers)
{
Console.WriteLine(number); // Output: 1, 2, 3
}
}
}
<Bad Code>
csharpusing System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a List to store elements
List<int> numbers = new List<int>();
// Add elements to the List
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
numbers.Add(1); // Duplicate, will be added
// Display the elements
foreach (int number in numbers)
{
Console.WriteLine(number); // Output: 1, 2, 3, 1
}
}
}
<Memo>
HashSet is part of the System.Collections.Generic namespace and is
implemented using a hash table, providing average time complexity of O(1)
for add, remove, and contains operations.
90
Use Queue and Stack for FIFO and LIFO
collections.
Queue and Stack are specialized collections for First-In-First-Out (FIFO)
and Last-In-First-Out (LIFO) operations, respectively.
In C#, Queue and Stack are used to manage collections where the order of
processing elements is important. Queue processes elements in the order
they were added (FIFO), while Stack processes elements in reverse order
(LIFO).
< Good Code >
csharpusing System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Queue example (FIFO)
Queue<string> queue = new Queue<string>();
queue.Enqueue("First");
queue.Enqueue("Second");
queue.Enqueue("Third");
while (queue.Count > 0)
{
Console.WriteLine(queue.Dequeue()); // Output: First, Second,
Third
}
// Stack example (LIFO)
Stack<string> stack = new Stack<string>();
stack.Push("First");
stack.Push("Second");
stack.Push("Third");
while (stack.Count > 0)
{
Console.WriteLine(stack.Pop()); // Output: Third, Second, First
}
}
}
<Bad Code>
csharpusing System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// List example used incorrectly for FIFO
List<string> list = new List<string>();
list.Add("First");
list.Add("Second");
list.Add("Third");
while (list.Count > 0)
{
Console.WriteLine(list[0]);
list.RemoveAt(0); // Output: First, Second, Third
}
// List example used incorrectly for LIFO
list.Add("First");
list.Add("Second");
list.Add("Third");
while (list.Count > 0)
{
Console.WriteLine(list[list.Count - 1]);
list.RemoveAt(list.Count - 1); // Output: Third, Second, First
}
}
}
The good example uses Queue and Stack, which are designed for FIFO and
LIFO operations, respectively. The bad example uses List, which can
achieve similar results but is less efficient and not semantically appropriate
for these operations.
<Memo>
Queue and Stack are part of the System.Collections.Generic namespace.
Queue is implemented as a circular array, while Stack is implemented as an
array, both providing O(1) time complexity for their primary operations
(Enqueue/Dequeue for Queue and Push/Pop for Stack).
91
Use try-catch blocks to handle
exceptions.
Encapsulate code that may throw exceptions in try-catch blocks to handle
errors gracefully.
Using try-catch blocks allows you to manage runtime errors and maintain
program stability by catching exceptions and providing appropriate
responses.
< Good Code >
try
{
// Code that may throw an exception
int result = 10 / int.Parse(userInput);
}
catch (FormatException ex)
{
// Handle format exception
Console.WriteLine("Please enter a valid number.");
}
catch (DivideByZeroException ex)
{
// Handle divide by zero exception
Console.WriteLine("Division by zero is not allowed.");
}
<Bad Code>
Good code example: The try block contains code that might throw
exceptions, and the catch blocks handle specific exceptions
(FormatException, DivideByZeroException). This ensures that the program
can handle errors without crashing.
Bad code example: No try-catch block is used, so any exception thrown by
the code (like FormatException or DivideByZeroException) will crash the
program.
<Memo>
Exception handling in C# provides a way to react to exceptional
circumstances (like runtime errors) in a controlled fashion, making
programs more robust and easier to debug.
92
Log exceptions for debugging purposes.
Log exceptions to understand and debug issues effectively.
try
{
// Code that may throw an exception
int result = 10 / int.Parse(userInput);
}
catch (Exception ex)
{
// Log the exception details
File.WriteAllText("log.txt", $"Exception: {ex.Message} at
{ex.StackTrace}");
Console.WriteLine("An error occurred. Please check the log for
details.");
}
<Bad Code>
try
{
// Code that may throw an exception
int result = 10 / int.Parse(userInput);
}
catch (Exception ex)
{
// Only display a generic message
Console.WriteLine("An error occurred.");
}
Good code example: The catch block logs the exception details (message
and stack trace) to a file. This helps in diagnosing the issue later by
reviewing the log file.
Bad code example: The catch block only displays a generic error message
without logging any details, making it harder to debug and understand the
cause of the issue.
<Memo>
Logging is a crucial aspect of software development, providing a way to
track application behavior and errors, which is essential for maintenance
and debugging in production environments.
93
Use custom exceptions for specific error
conditions.
Create custom exception classes to handle specific error conditions in your
application.
<Bad Code>
<Memo>
Custom exceptions can be extended to include additional properties and
methods, providing even more context and functionality for error handling.
94
Validate method arguments to prevent
errors.
Always validate method arguments to ensure they meet the expected criteria
before processing.
Validating method arguments helps prevent errors and ensures that your
methods operate on valid data, improving the robustness of your code.
< Good Code >
<Bad Code>
<Memo>
Argument validation can be automated using code contracts or validation
libraries, which can further simplify and standardize the validation process.
95
Use finally blocks to clean up resources.
Ensure that resources are always cleaned up, even if an exception occurs,
by using finally blocks.
Using finally blocks is a best practice for cleaning up resources such as file
handles, database connections, or network sockets in your C# programs.
< Good Code >
<Bad Code>
StreamReader reader = new StreamReader("example.txt");
try
{
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
catch (Exception ex)
{
Console.WriteLine("An error occurred: " + ex.Message);
}
// No finally block to ensure the reader is closed
In the good example, the finally block ensures that the StreamReader is
closed regardless of whether an exception occurs. This prevents potential
resource leaks and ensures the application runs efficiently. In the bad
example, if an exception is thrown, the StreamReader may not be closed
properly, leading to resource leaks.
<Memo>
The finally block is executed after the try and catch blocks, no matter what.
It's a feature of the .NET framework to guarantee that cleanup code always
runs.
96
Write reusable methods for common
tasks.
Create methods for tasks that are repeated throughout your code to improve
readability and maintainability.
<Bad Code>
class Program
{
static void Main()
{
string rawString1 = " hello world ";
string formattedString1 = rawString1.Trim().ToUpper();
Console.WriteLine(formattedString1); // Output: HELLO WORLD
string rawString2 = " csharp programming ";
string formattedString2 = rawString2.Trim().ToUpper();
Console.WriteLine(formattedString2); // Output: CSHARP
PROGRAMMING
}
}
<Memo>
Encapsulating common tasks into reusable methods not only improves code
quality but also adheres to the DRY (Don't Repeat Yourself) principle,
which is fundamental in software development to reduce redundancy and
errors.
97
Use interfaces to define contracts for
classes.
Interfaces define a contract that classes must follow, ensuring consistency
and promoting loose coupling.
<Bad Code>
<Memo>
Interfaces in C# can also inherit from other interfaces, allowing for the
creation of more complex and hierarchical contracts.
98
Leverage inheritance to reuse code.
Inheritance allows classes to inherit methods and properties from a base
class, promoting code reuse and reducing redundancy.
<Bad Code>
In the good example, the Animal base class contains common methods Eat
and Sleep, which are inherited by both Dog and Bird classes. This reduces
code duplication and makes the code easier to maintain. In the bad example,
the Eat and Sleep methods are duplicated in both Dog and Bird classes,
leading to redundancy and potential maintenance issues.
<Memo>
C# supports single inheritance, meaning a class can only inherit from one
base class. However, a class can implement multiple interfaces, allowing for
more flexible design.
99
Use generics to create flexible and
reusable code.
Generics allow you to define classes, methods, and interfaces with a
placeholder for the type of data they store or use, making your code more
flexible and reusable.
Generics in C# enable you to write a class or method that can work with
any data type. This reduces code duplication and increases type safety.
< Good Code >
<Memo>
Generics were introduced in C# 2.0 and are a powerful feature for creating
type-safe data structures and methods.
100
Encapsulate common functionality in
utility classes.
Utility classes group common functions that can be reused across different
parts of an application, promoting code reuse and maintainability.
Utility classes in C# are static classes that contain static methods for
common operations, reducing code duplication and improving code
organization.
< Good Code >
<Memo>
Utility classes are often used for operations like string manipulation, file
handling, and mathematical calculations, providing a centralized place for
common functionality.
101
Use dependency injection to manage
dependencies.
Dependency injection (DI) helps manage dependencies in a clean and
maintainable way.
Using DI allows you to inject dependencies into a class, making the code
more modular and easier to test.
< Good Code >
<Bad Code>
<Memo>
Dependency Injection is a core principle of the SOLID design principles,
specifically the Dependency Inversion Principle (DIP).
102
Write unit tests to ensure code
correctness.
Unit tests help verify that individual parts of your code work as expected.
Writing unit tests ensures that your code behaves correctly and helps catch
bugs early in the development process.
< Good Code >
<Bad Code>
In the good example, the Calculator class has a corresponding unit test that
verifies the Add method works correctly. This automated test ensures that
any changes to the code can be quickly verified. In the bad example, there
are no unit tests, and the functionality is manually tested, which is error-
prone and not scalable.
<Memo>
Unit tests are a fundamental part of Test-Driven Development (TDD),
where tests are written before the actual code implementation.
Afterword
◆
Thank you for reading through this comprehensive guide on writing cleaner
and more readable code in C#.
By leveraging the unique features and idioms of C#, these methods are
designed to help you write code that is not only functional but also easy for
others to understand and maintain.
Each technique has been illustrated with examples of both good and bad
code, providing clear and practical insights into best practices.
Our goal has been to make the content as accessible and useful as possible,
ensuring that you can apply these principles directly to your work.
We hope that this book has equipped you with the knowledge and tools to
write better C# code and that it will serve as a valuable resource in your
ongoing development journey.
Thank you once again for your commitment to writing better code.