Conditional Statements: if, else if, else.
In C#, you can use special tools called control statements to choose which pieces of code to run. The most important tool for this is the “if
statement.” It checks if something is true or not and then decides what code should be done based on that.
The if Statement
The if
statement evaluates a Boolean expression. If the expression results in true
, the code inside the curly braces {}
of the if
statement gets executed.
if (condition)
{
// Code to be executed if condition is true
}
Example:
int age = 25;
if (age > 18)
{
Console.WriteLine("You are an adult.");
}
The else Statement
The else
statement is used in conjunction with the if
statement. The code inside its block is executed if the if
condition is false.
if (condition)
{
// Code to be executed if condition is true
}
else
{
// Code to be executed if condition is false
}
Example:
int age = 15;
if (age > 18)
{
Console.WriteLine("You are an adult.");
}
else
{
Console.WriteLine("You are a minor.");
}
The else if Statement
For situations where you have multiple conditions to check sequentially, you use the else if
statement. It provides an additional conditional check after an if
or another else if
.
if (condition1)
{
// Code to be executed if condition1 is true
}
else if (condition2)
{
// Code to be executed if condition2 is true
}
else
{
// Code to be executed if none of the above conditions are true
}
Example:
int score = 85;
if (score >= 90)
{
Console.WriteLine("Grade: A");
}
else if (score >= 80)
{
Console.WriteLine("Grade: B");
}
else if (score >= 70)
{
Console.WriteLine("Grade: C");
}
else
{
Console.WriteLine("Grade: F");
}
Conditional Operator (?:)
Apart from the traditional if
, else if
, and else
constructs, C# also offers a concise ternary conditional operator.
condition ? resultIfTrue : resultIfFalse;
Example:
int age = 20;
string status = age >= 18 ? "Adult" : "Minor";
Console.WriteLine(status); // Outputs: Adult
This operator is particularly useful for short conditions but can reduce readability if overused or applied to complex conditions.
Using Logical Operators
Logical operators (&&
, ||
, and !
) can be combined with conditional statements to create more complex conditions.
&&
: Logical AND||
: Logical OR!
: Logical NOT
Example:
int age = 25;
bool hasLicense = true;
if (age >= 18 && hasLicense)
{
Console.WriteLine("Allowed to drive.");
}
Null-conditional Operators
Introduced in C# 6, the null-conditional operator ?.
allows for concise null checks.
var result = object?.Property;
This returns null
if the object is null
, otherwise it returns the property’s value.
Pattern Matching in if (Introduced in C# 7)
You can use patterns in the is
expression and in the switch
statement.
object obj = "Hello";
if (obj is string str)
{
Console.WriteLine($"String of length {str.Length}");
}
Best Practices
- Readability: Ensure that your conditions are readable. Complex conditions can be broken down into methods or variables with meaningful names.
- Keep it short: A long list of
else if
conditions can make your code hard to follow. Consider refactoring into a switch statement or using a different design approach if you have numerous checks. - Beware of side effects: Avoid having side effects in your conditional checks. For instance, altering a variable as part of a condition check can make your code harder to debug.
- Avoid Deep Nesting: Deeply nested
if
statements can be confusing. Try to refactor the logic to reduce nesting or use other constructs likeswitch
statements. - Explicit over Implicit: Always try to make your conditions explicit. For instance, prefer
(count > 0)
over(count)
. - Evaluate Performance: Sometimes the order of conditions can impact performance, especially when using short-circuiting (
&&
and||
). Place conditions that are more likely to be false (and hence exit the check early) at the beginning.
Switch Case
The switch
statement is a type of selection statement that allows you to choose a block of code to execute from several alternatives. It’s often a cleaner alternative to a series of nested if-else
statements, especially when dealing with discrete values.
Traditional Switch Syntax
In C#, the switch
statement has been a staple for multi-branch conditional logic since the inception of the language. It provides a way to select one of many code blocks to execute based on the value of an expression.
Syntax:
switch (expression)
{
case value1:
// Code to execute for value1
break;
case value2:
// Code to execute for value2
break;
// ... other cases ...
default:
// Code to execute if no case matches
break;
}
Key Components:
expression
: This is evaluated once and its result is compared against the values in the case labels.case
: Eachcase
label represents a possible value of the expression. The associated code block is executed if theexpression
matches the case value.break
: It is used to terminate theswitch
statement. Omitting abreak
can lead to unintentional “fall-through” behavior where multiple blocks execute sequentially, though C# explicitly requires an exit keyword (break
,return
,goto
, orthrow
) to avoid this behavior.default
: This is an optional block that executes when theexpression
doesn’t match any of the provided case values. It acts as a catch-all.
Example:
int dayOfWeek = 3;
switch (dayOfWeek)
{
case 1:
Console.WriteLine("Monday");
break;
case 2:
Console.WriteLine("Tuesday");
break;
case 3:
Console.WriteLine("Wednesday");
break;
default:
Console.WriteLine("Another day of the week");
break;
}
In the above example, the code will output “Wednesday” since the value of dayOfWeek
is 3
. While the traditional switch syntax is powerful and often straightforward, it does come with limitations:
- Only constant values or values known at compile-time can be used as case labels.
- It can become verbose and harder to read when there are many case branches.
Despite these limitations, the traditional switch syntax has served developers well for many years, providing a clear structure for branching based on discrete values.
Pattern Matching (Enhanced in C# 7 and later)
Pattern matching allows for more expressive switch
statements. For instance, type patterns allow you to match on the runtime type of the expression.
object obj = "Hello";
switch (obj)
{
case int i:
Console.WriteLine($"It's an integer with value {i}");
break;
case string s:
Console.WriteLine($"It's a string with value {s}");
break;
default:
Console.WriteLine("Unknown type");
break;
}
Switch Expressions (Introduced in C# 8)
With C# 8, switch
became even more powerful and concise. It introduced switch expressions, which allow for more compact switch constructs:
var result = expression switch
{
pattern1 => result1,
pattern2 => result2,
_ => defaultResult
};
Property Pattern (C# 8 and later)
You can test an object against nested properties:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
var person = new Person { Name = "John", Age = 30 };
var result = person switch
{
{ Age: < 20 } => "Young",
{ Age: >= 20 and < 50 } => "Adult",
_ => "Senior"
};
Tuple Patterns (C# 8 and later)
Allows you to switch based on tuple values.
(int X, int Y) point = (5, 10);
var location = point switch
{
(0, 0) => "Origin",
(_, 0) => "On X-axis",
(0, _) => "On Y-axis",
_ => "Elsewhere"
};
Relational Patterns (Introduced in C# 9)
These allow you to express numerical relationships in a more concise manner within switch
patterns:
int age = 25;
var classification = age switch
{
< 13 => "Child",
< 20 => "Teenager",
< 65 => "Adult",
_ => "Senior"
};
Logical Patterns (C# 9)
You can combine patterns with logical operators and
, or
, and not
:
char grade = 'B';
var isPass = grade switch
{
'A' or 'B' or 'C' => true,
'D' or 'F' => false,
_ => throw new InvalidOperationException("Invalid grade")
};
Using when Clauses
The when
keyword adds an additional filter to your case
label, allowing for more refined conditions:
int number = 15;
switch (number)
{
case int n when n % 2 == 0:
Console.WriteLine("Even");
break;
case int n when n % 2 != 0:
Console.WriteLine("Odd");
break;
default:
throw new InvalidOperationException("Unexpected input");
}
Best Practices
- Avoid Fall-Throughs: Historically, in some languages, it’s easy to forget the
break
statement, leading to accidental fall-through between cases. C# requires explicit flow control in each case, so always remember to include abreak
,return
,throw
, etc. - Use the Default Case: Always include a
default
case to handle unexpected values, even if you think every possible value is covered. This enhances robustness. - Avoid Large
switch
Blocks: If you have a very largeswitch
block, consider whether there’s a more maintainable approach, such as using a dictionary lookup or even a strategy pattern. - Performance Considerations: In some scenarios, a
switch
statement might be optimized better than a corresponding series ofif-else
statements, leading to faster code execution. - Maintainability: As you evolve your codebase, ensure that your
switch
statements are updated to accommodate any new potential values for the switched expression.
The continuous enhancements to the switch
statement in C# have transformed it from a simple conditional branching mechanism to a potent and expressive tool. Expert developers know how to harness its full power, creating clean, efficient, and robust branching logic in their applications. It’s essential to stay updated with the language’s evolution and best practices to make the most out of these constructs.
Loops: for, while, do-while, foreach
Looping constructs are fundamental to most programming languages, allowing for the repeated execution of code based on specific conditions or collections. Here’s a comprehensive overview of loops in C#.
for Loop
The for
loop provides a concise way to iterate a set number of times. It consists of an initializer, a condition, and an iterator.
Syntax:
for (initializer; condition; iterator)
{
// Code to execute on each iteration
}
Example:
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
}
Nuances:
- Commonly used for situations where you know the number of iterations in advance.
- All three components (initializer, condition, iterator) are optional, but the semicolons must remain.
while Loop
The while
loop executes its body as long as a condition remains true.
Syntax:
while (condition)
{
// Code to execute
}
Example:
int count = 5;
while (count > 0)
{
Console.WriteLine(count);
count--;
}
Nuances:
- If the condition is initially false, the loop body might never execute.
- Suitable for scenarios where the number of iterations is not known in advance.
do-while Loop
Similar to the while
loop but checks the condition after executing the loop’s body, guaranteeing at least one execution.
Syntax:
do
{
// Code to execute
} while (condition);
Example:
int value;
do
{
Console.WriteLine("Enter a number (0 to exit):");
value = int.Parse(Console.ReadLine());
} while (value != 0);
Nuances:
- Ideal for scenarios where you want the loop body to execute at least once, like user input validation.
foreach Loop
The foreach
loop iterates over a collection or array, assigning each element to a variable in succession.
Syntax:
foreach (varType item in collection)
{
// Code to execute
}
Example:
string[] colors = { "Red", "Green", "Blue" };
foreach (string color in colors)
{
Console.WriteLine(color);
}
Nuances:
- Designed for collections, so you don’t need to manage an index variable.
- Under the hood, it uses the IEnumerable and IEnumerator interfaces.
Infinite Loops
These are loops that run indefinitely due to conditions that never become false. They can be intentional (e.g., server loops that wait for client connections) or unintentional due to bugs.
while (true)
{
// Infinite loop
}
Nested Loops
One loop inside another is termed a nested loop. The inner loop completes its iterations before the next iteration of the outer loop.
for (int i = 0; i < 5; i++)
{
for (int j = 0; j < 5; j++)
{
Console.WriteLine($"i: {i}, j: {j}");
}
}
Looping Through Multi-Dimensional Arrays
Arrays can have more than one dimension, typically used for matrices or tables. Nested loops can iterate through these.
int[,] matrix = new int[3, 3]
{
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
for (int x = 0; x < matrix.GetLength(0); x++)
{
for (int y = 0; y < matrix.GetLength(1); y++)
{
Console.WriteLine(matrix[x, y]);
}
}
The yield Keyword
Introduced in C# 2.0, yield
helps in custom iteration scenarios, especially with IEnumerable
and IEnumerator
. It can be used to create custom iterators without the need for explicit temp collections.
public IEnumerable<int> GetNumbers()
{
for (int i = 0; i < 10; i++)
{
if (i % 2 == 0)
{
yield return i;
}
}
}
Parallel Loops
With the introduction of the Task Parallel Library (TPL) in .NET, you can easily parallelize loops for performance gains in multi-core processors using Parallel.For
and Parallel.ForEach
.
Parallel.For(0, 100000, i =>
{
// Do work in parallel
});
Best Practices & Insights
- Performance: When working with certain collection types (e.g., List<T>), the
for
loop might offer better performance thanforeach
due to avoiding the enumerator overhead. - Avoid Modifications: Don’t modify the collection you’re iterating over during a
foreach
loop. This can lead to runtime exceptions. If you need to make changes, consider iterating over a copy of the collection or using other strategies. - Variable Scope: In a
for
loop, the loop variable (likei
in our example) is limited to the scope of the loop. However, in aforeach
loop, the loop variable retains its value even after the loop completes. - Early Exit: If you find what you’re looking for or meet a certain condition, use the
break
keyword to exit a loop early. To skip an iteration and continue with the next, usecontinue
. - Prefer ++i over i++ in Loops: Due to subtle differences in post-increment and pre-increment,
++i
might offer slight performance benefits in some scenarios. - Reduce Overhead: If repeatedly calling methods (like
collection.Count()
), consider storing results in a variable outside the loop. - Consider Data Structures: Some data structures are more efficient for specific operations. For example, using a
HashSet<T>
when checking for existence can be faster than looping through aList<T>
.
Loops, while being foundational programming constructs, can be nuanced in behavior, especially with advancements in the language and platform. To craft efficient, maintainable, and bug-free loops, a developer should not only understand the basic syntax but also the underlying mechanics and available advanced features. As with any tool, the effectiveness of loops in C# depends largely on their judicious and informed use.
FAQ
The if
statement evaluates a boolean expression. If the expression is true
, the code block inside the if
statement is executed.
if (expression)
{
// Code to execute if expression is true
}
Yes, you can combine multiple conditions using logical operators like &&
(and), ||
(or), and !
(not).
if (age > 18 && hasLicense)
{
// Drive the car
}
Yes, you can nest if statements to create more complex decision-making structures. This is known as nested conditional statements.
if (loggedIn)
{
if (isAdmin)
{
// Admin-specific code
}
else
{
// Regular user code
}
}
The break
keyword exits the switch
statement. If omitted (and without other control statements like return
), the execution “falls through” to subsequent case
blocks, which can lead to unintended behavior.
Yes. The break
keyword exits the loop prematurely, and the continue
keyword skips the remainder of the current iteration and proceeds to the next one.
Some tips include:
1. Avoid heavy computations in the loop condition, especially if they don’t change (e.g., for (int i = 0; i < ComputeMaxValue(); i++)
).
2. For collections, using foreach
can be more efficient than using for
with an index, especially with certain collection types.
3. Limit the scope of variables to the loop if they aren’t needed elsewhere.