On this page

Control flow and functions in C# 14

14 min read TextCh. 1 — C# Fundamentals

Control flow in C#

Control flow determines the order in which statements execute. C# offers the classic structures plus expressive modern updates.

if / else

The most basic structure for making decisions:

int temperature = 35;

if (temperature > 40)
{
    Console.WriteLine("Extreme heat");
}
else if (temperature > 30)
{
    Console.WriteLine("Strong heat");   // ← executes
}
else if (temperature > 20)
{
    Console.WriteLine("Pleasant temperature");
}
else
{
    Console.WriteLine("Cold");
}

// Single-line if (no braces) — only when obvious
if (temperature > 0) Console.WriteLine("Above zero");

Conditional expression (ternary)

bool isAdult = age >= 18;
string message = isAdult ? "Welcome" : "Access denied";

// Nested ternary (use sparingly)
string grade = points >= 90 ? "A" : points >= 70 ? "B" : points >= 50 ? "C" : "F";

switch statement

To compare a value against multiple options:

string dayName = "Monday";

switch (dayName)
{
    case "Monday":
    case "Tuesday":
    case "Wednesday":
    case "Thursday":
    case "Friday":
        Console.WriteLine("Weekday");
        break;
    case "Saturday":
    case "Sunday":
        Console.WriteLine("Weekend");
        break;
    default:
        Console.WriteLine("Invalid day");
        break;
}

switch expressions (C# 8+, improved in C# 14)

The modern, more expressive version. Returns a value directly:

// Over enums — the compiler verifies exhaustiveness
enum Status { Active, Inactive, Suspended, Deleted }

string DescribeStatus(Status status) => status switch
{
    Status.Active    => "User can access",
    Status.Inactive  => "Account temporarily deactivated",
    Status.Suspended => "Account suspended for violation",
    Status.Deleted   => "Account permanently deleted"
    // No default: the compiler warns if an enum case is missing
};

// With property patterns
record Order(decimal Total, string Country, bool IsPremium);

string CalculateShipping(Order o) => o switch
{
    { Total: > 100, Country: "US" }  => "Free shipping",
    { IsPremium: true }              => "Free shipping (premium)",
    { Country: "US", Total: > 50 }   => "Discounted shipping",
    _                                => "Standard shipping"
};

Loops

for — when you know the number of iterations

// Multiplication table of 5
for (int i = 1; i <= 10; i++)
{
    Console.WriteLine($"5 × {i} = {5 * i}");
}

// Reverse iteration
for (int i = 10; i >= 1; i--)
{
    Console.Write($"{i} ");
}
Console.WriteLine();

// for with multiple variables
for (int i = 0, j = 10; i < j; i++, j--)
{
    Console.WriteLine($"i={i}, j={j}");
}

foreach — for collections

string[] languages = { "C#", "Go", "Rust", "Python", "TypeScript" };

foreach (string lang in languages)
{
    Console.WriteLine($"  → {lang}");
}

// foreach with index using LINQ
foreach (var (lang, idx) in languages.Select((l, i) => (l, i)))
{
    Console.WriteLine($"{idx + 1}. {lang}");
}

while — while a condition holds

int attempts  = 0;
int maxTries  = 3;
bool success  = false;

while (attempts < maxTries && !success)
{
    Console.Write("Enter the password: ");
    string pwd = Console.ReadLine() ?? "";
    attempts++;

    if (pwd == "dotnet10")
    {
        success = true;
        Console.WriteLine("Access granted!");
    }
    else
    {
        Console.WriteLine($"Wrong password. Attempts: {attempts}/{maxTries}");
    }
}

do-while — executes at least once

int number;
do
{
    Console.Write("Enter a positive number: ");
} while (!int.TryParse(Console.ReadLine(), out number) || number <= 0);

Console.WriteLine($"Valid number: {number}");

break, continue, and return

// break — exits the loop
for (int i = 0; i < 100; i++)
{
    if (i == 5) break;
    Console.Write($"{i} "); // 0 1 2 3 4
}

// continue — skips to the next iteration
for (int i = 0; i < 10; i++)
{
    if (i % 2 == 0) continue; // skip even numbers
    Console.Write($"{i} "); // 1 3 5 7 9
}

Methods (functions)

In C#, functions are called methods and always belong to a class or struct. The Program.cs entry point uses top-level statements, but internally it is also a method.

// Method with return type
static int Add(int a, int b)
{
    return a + b;
}

// Expression-bodied method (single expression)
static int Multiply(int a, int b) => a * b;

// void method (no return)
static void Greet(string name)
{
    Console.WriteLine($"Hello, {name}!");
}

// Optional parameters (must come last)
static string Format(string text, bool uppercase = false, char separator = ' ')
{
    string result = text.Trim();
    if (uppercase) result = result.ToUpper();
    return result.Replace(' ', separator);
}

Console.WriteLine(Format("hello world"));           // "hello world"
Console.WriteLine(Format("hello world", true));     // "HELLO WORLD"
Console.WriteLine(Format("hello world", true, '-')); // "HELLO-WORLD"

Named arguments

// Named arguments improve readability
string result = Format(
    text:      "hello world",
    separator: '_',
    uppercase: true
);
Console.WriteLine(result); // "HELLO_WORLD"

Local functions

Local functions are methods defined inside another method. They are only visible within the parent method:

static string ProcessOrder(string[] items, decimal discount)
{
    // Local function — only visible here
    decimal CalculateTotal()
    {
        decimal sum = 0;
        foreach (string item in items)
            sum += GetPrice(item);
        return sum * (1 - discount);
    }

    decimal GetPrice(string item) => item switch
    {
        "laptop"   => 999.99m,
        "mouse"    => 29.99m,
        "keyboard" => 79.99m,
        _          => 0m
    };

    decimal total = CalculateTotal();
    return $"Total: ${total:F2}";
}

Practice

  1. FizzBuzz: Use a for loop from 1 to 100. If the number is divisible by 3, print "Fizz"; by 5, "Buzz"; by both, "FizzBuzz"; otherwise the number.
  2. Calculator with switch expression: Create a method Calculate(double a, double b, string operation) that uses a switch expression for +, -, *, / and returns the result.
  3. params: Implement a method Average(params double[] values) that returns the average of the received values.

In the next lesson we will learn how to define classes and objects — the core of object-oriented programming in C#.

Prefer switch expressions over switch statements
Switch expressions are more concise, force you to cover all cases, and can be used anywhere a value is expected (in assignments, return statements, etc.). The compiler warns when cases are missing for an enum type.
Local functions vs lambdas
Use local functions when you need recursion, when the code is complex, or when you want a descriptive name. Use lambdas when the code is very short and used only once (for example, in LINQ).
ref and out in production
Avoid ref and out in public APIs — they complicate the method signature and the calling code. Use them in performance-critical situations or when interacting with existing APIs that require them (such as TryParse).
// Switch expression — C# 8+ refined in C# 14
string DayOfWeekName(int day) => day switch
{
    1 => "Monday",
    2 => "Tuesday",
    3 => "Wednesday",
    4 => "Thursday",
    5 => "Friday",
    6 => "Saturday",
    7 => "Sunday",
    _ => throw new ArgumentOutOfRangeException(nameof(day), "Invalid day")
};

// Switch expression with guards (when clauses)
string ClassifyAge(int age) => age switch
{
    < 0             => throw new ArgumentException("Invalid age"),
    < 13            => "Child",
    >= 13 and < 18  => "Teenager",
    >= 18 and < 65  => "Adult",
    >= 65           => "Senior"
};

Console.WriteLine(DayOfWeekName(3));     // Wednesday
Console.WriteLine(ClassifyAge(25));       // Adult