Fundamentals

Потік Керування

Вивчення умовних операторів, циклів, операторів переходу та зіставлення шаблонів у C# для управління виконанням програми

Потік Керування (Control Flow)

Вступ

Уявіть, що ви створюєте додаток для онлайн-магазину. Користувач додає товар до кошика, і програма має прийняти рішення: чи достатньо товару на складі? Якщо так — додати в кошик, якщо ні — показати повідомлення про відсутність. Далі програма має перевірити всі товари в кошику, обчислити загальну суму з урахуванням знижок та податків. Кожна з цих дій потребує потоку керування (control flow) — механізму, який визначає, які інструкції виконуються, в якому порядку та за яких умов.

Потік керування — це фундаментальна концепція програмування, яка дозволяє створювати динамічні та інтелектуальні програми. Без нього код виконувався б лише послідовно, рядок за рядком, що унеможливлювало б будь-яку складну логіку.

Потік керування (Control Flow) — це порядок, у якому виконуються окремі інструкції, вирази або виклики функцій у програмі. Він визначається за допомогою умовних операторів, циклів та операторів переходу. Докладніше у офіційній документації.

Що ви дізнаєтесь

У цьому розділі ви опануєте:

  • Умовні оператори для прийняття рішень (if, else, switch, тернарний оператор)
  • Цикли для повторення дій (for, foreach, while, do-while)
  • Оператори переходу для контролю виконання (break, continue, return)
  • Зіставлення шаблонів у switch виразах для елегантної обробки складних умов

Передумови

Перед вивченням цього розділу рекомендується ознайомитися з:


Умовні Оператори

Умовні оператори дозволяють програмі приймати рішення на основі певних умов. Це фундамент логіки будь-якої програми.

Оператор if

Найпростіший спосіб виконати код залежно від умови — це оператор if.

Синтаксис:

if (умова)
{
    // Код, що виконується, якщо умова істинна
}

Приклад:

int temperature = 25;

if (temperature > 20)
{
    Console.WriteLine("Сьогодні тепло!");
}
Завжди використовуйте фігурні дужки {} навіть для однорядкових блоків коду. Це покращує читабельність та запобігає помилкам при подальшому редагуванні коду.

Оператор if-else

Для обробки альтернативного сценарію використовується else:

int age = 16;

if (age >= 18)
{
    Console.WriteLine("Ви можете голосувати.");
}
else
{
    Console.WriteLine("Ви ще не досягли віку для голосування.");
}

Оператор else if

Для перевірки кількох умов послідовно:

int score = 85;

if (score >= 90)
{
    Console.WriteLine("Оцінка: Відмінно");
}
else if (score >= 75)
{
    Console.WriteLine("Оцінка: Добре");
}
else if (score >= 60)
{
    Console.WriteLine("Оцінка: Задовільно");
}
else
{
    Console.WriteLine("Оцінка: Незадовільно");
}

Вкладені умовні оператори

Умовні оператори можна вкладати один в одного:

int temperature = 22;
bool isRaining = false;

if (temperature > 20)
{
    if (isRaining)
    {
        Console.WriteLine("Тепло, але дощить. Візьміть парасольку!");
    }
    else
    {
        Console.WriteLine("Чудова погода для прогулянки!");
    }
}
else
{
    Console.WriteLine("Холодно. Одягніться тепліше.");
}
Надмірне вкладання умовних операторів (більше 2-3 рівнів) погіршує читабельність коду. Розгляньте можливість рефакторингу через раннє повернення (early return) або винесення логіки в окремі методи.

Тернарний Оператор

Тернарний оператор ?: — це скорочена форма if-else, яка повертає значення:

Синтаксис:

умова ? значення_якщо_істина : значення_якщо_хиба

Приклад:

int age = 20;
string status = age >= 18 ? "Дорослий" : "Неповнолітній";
Console.WriteLine(status); // Виведе: Дорослий
int number = 42;
string result;

if (number % 2 == 0)
{
    result = "Парне";
}
else
{
    result = "Непарне";
}
Використовуйте тернарний оператор для простих умов, які повертають значення. Для складної логіки краще використовувати звичайний if-else для покращення читабельності.

Оператор switch

Оператор switch використовується для багатоваріантного вибору на основі значення виразу.

Синтаксис:

switch (вираз)
{
    case значення1:
        // Код для значення1
        break;
    case значення2:
        // Код для значення2
        break;
    default:
        // Код за замовчуванням
        break;
}

Приклад:

int dayOfWeek = 3;

switch (dayOfWeek)
{
    case 1:
        Console.WriteLine("Понеділок");
        break;
    case 2:
        Console.WriteLine("Вівторок");
        break;
    case 3:
        Console.WriteLine("Середа");
        break;
    case 4:
        Console.WriteLine("Четвер");
        break;
    case 5:
        Console.WriteLine("П'ятниця");
        break;
    case 6:
        Console.WriteLine("Субота");
        break;
    case 7:
        Console.WriteLine("Неділя");
        break;
    default:
        Console.WriteLine("Некоректний день тижня");
        break;
}

Множинні мітки case

Можна об'єднувати кілька міток case для виконання одного блоку коду:

int dayOfWeek = 6;

switch (dayOfWeek)
{
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
        Console.WriteLine("Робочий день");
        break;
    case 6:
    case 7:
        Console.WriteLine("Вихідний");
        break;
    default:
        Console.WriteLine("Некоректний день");
        break;
}
У C# немає "провалювання" (fall-through) між блоками case, як у деяких інших мовах. Кожен case має закінчуватися оператором break, return, goto або throw.

Порівняння умовних конструкцій

КонструкціяКоли використовуватиПеревагиНедоліки
if-elseУмови на основі булевих виразів, складна логікаУніверсальність, зрозумілістьБагато коду для простих випадків
Тернарний операторПрості умови з поверненням значенняКомпактністьПогана читабельність при вкладанні
switchПеревірка одного виразу на множину значеньЧитабельність для багатьох варіантівОбмежений тип виразу (до C# 7)
switch виразЯк switch, але з поверненням значенняКомпактність, pattern matchingПотребує C# 8.0+

Цикли

Цикли дозволяють повторювати блок коду кілька разів. Це критично важливо для обробки колекцій, повторення дій та реалізації алгоритмів.

Цикл for

Цикл for використовується, коли кількість ітерацій відома заздалегідь.

Синтаксис:

for (ініціалізація; умова; ітератор)
{
    // Код, що повторюється
}

Компоненти:

  • Ініціалізація: виконується один раз перед початком циклу
  • Умова: перевіряється перед кожною ітерацією
  • Ітератор: виконується після кожної ітерації

Приклад:

for (int i = 0; i < 5; i++)
{
    Console.WriteLine($"Ітерація: {i}");
}
// Виведе:
// Ітерація: 0
// Ітерація: 1
// Ітерація: 2
// Ітерація: 3
// Ітерація: 4

Множинні вирази в for

Можна використовувати кілька змінних:

for (int i = 0, j = 10; i < j; i++, j--)
{
    Console.WriteLine($"i = {i}, j = {j}");
}
Цикл for ідеально підходить для роботи з масивами, коли потрібен доступ до індексу елемента.

Цикл foreach

Цикл foreach використовується для ітерації по колекціях без необхідності маніпулювати індексом.

Синтаксис:

foreach (тип змінна in колекція)
{
    // Код, що використовує змінну
}

Приклад:

string[] fruits = { "Яблуко", "Банан", "Апельсин", "Груша" };

foreach (string fruit in fruits)
{
    Console.WriteLine($"Фрукт: {fruit}");
}

Неявна типізація з var

Компілятор може автоматично визначити тип:

var numbers = new[] { 1, 2, 3, 4, 5 };

foreach (var number in numbers)
{
    Console.WriteLine(number * 2);
}
Цикл foreach працює з будь-яким типом, що реалізує інтерфейс IEnumerable або IEnumerable<T>. Елементи колекції доступні лише для читання всередині циклу.
int[] numbers = { 10, 20, 30, 40, 50 };

for (int i = 0; i < numbers.Length; i++)
{
    Console.WriteLine($"Індекс {i}: {numbers[i]}");
}

Цикл while

Цикл while виконується доки умова істинна. Він використовується, коли кількість ітерацій невідома наперед.

Синтаксис:

while (умова)
{
    // Код, що повторюється
}

Приклад:

int count = 0;

while (count < 5)
{
    Console.WriteLine($"Лічильник: {count}");
    count++;
}
Остерігайтесь нескінченних циклів! Переконайтеся, що умова циклу while колись стане хибною, інакше програма зависне.
// НЕ РОБІТЬ ТАК!
while (true)
{
    // Цей код виконуватиметься вічно
}

Цикл do-while

Цикл do-while схожий на while, але гарантує виконання тіла циклу принаймні один раз, оскільки умова перевіряється після виконання блоку коду.

Синтаксис:

do
{
    // Код, що повторюється
} while (умова);

Приклад:

int userInput;

do
{
    Console.Write("Введіть число більше 0: ");
    userInput = int.Parse(Console.ReadLine());
} while (userInput <= 0);

Console.WriteLine($"Ви ввели: {userInput}");
int count = 10;

while (count < 5)
{
    Console.WriteLine(count);
    count++;
}
// Код не виконається жодного разу

Порівняння типів циклів

Тип циклуКоли використовуватиОсобливості
forКількість ітерацій відома, потрібен індексКомпактний синтаксис, ініціалізація + умова + ітератор
foreachІтерація по колекції без індексуНайпростіший синтаксис, лише для читання
whileКількість ітерацій невідома, перевірка в початкуУмова перевіряється перед виконанням
do-whileТе саме що while, але потрібне мінімум 1 виконанняУмова перевіряється після виконання

Візуалізація виконання циклів

Loading diagram...
flowchart TD
    Start([Start]) --> Input[/Input: age/]
    Input --> Check{age >= 18?}
    Check -->|Yes| Adult["You are an adult"]
    Check -->|No| Minor["You are a minor"]
    Adult --> End([End])
    Minor --> End

    style Start fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style Input fill:#64748b,stroke:#334155,color:#ffffff
    style Check fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Adult fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style Minor fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style End fill:#64748b,stroke:#334155,color:#ffffff

Оператори Переходу

Оператори переходу (jump statements) дозволяють змінювати нормальний потік виконання програми. Вони можуть переривати цикли, пропускати ітерації або виходити з методів.

Оператор break

Оператор break припиняє виконання найближчого циклу або оператора switch.

Використання в циклі:

for (int i = 0; i < 10; i++)
{
    if (i == 5)
    {
        break; // Вихід з циклу при i = 5
    }
    Console.WriteLine(i);
}
// Виведе: 0 1 2 3 4

Використання у вкладених циклах:

for (int i = 0; i < 3; i++)
{
    for (int j = 0; j < 3; j++)
    {
        if (j == 1)
        {
            break; // Виходить лише з внутрішнього циклу
        }
        Console.WriteLine($"i={i}, j={j}");
    }
}
break припиняє лише найближчий цикл або switch. Для виходу з кількох вкладених циклів використовуйте прапорець (flag) або винесіть код у метод з return.

Оператор continue

Оператор continue пропускає решту коду в поточній ітерації циклу та переходить до наступної ітерації.

for (int i = 0; i < 10; i++)
{
    if (i % 2 == 0)
    {
        continue; // Пропустити парні числа
    }
    Console.WriteLine(i);
}
// Виведе: 1 3 5 7 9

Практичний приклад:

string[] files = { "document.txt", "image.png", "data.csv", "photo.jpg", "report.txt" };

foreach (string file in files)
{
    if (!file.EndsWith(".txt"))
    {
        continue; // Пропустити файли, що не є текстовими
    }
    Console.WriteLine($"Обробка текстового файлу: {file}");
}
// Виведе:
// Обробка текстового файлу: document.txt
// Обробка текстового файлу: report.txt
Використовуйте continue для покращення читабельності коду, щоб уникнути глибокого вкладання умовних операторів у циклах.

Оператор return

Оператор return припиняє виконання методу та повертає керування викликаючому коду. Він може повертати значення або бути порожнім (для методів типу void).

Повернення значення:

int Add(int a, int b)
{
    return a + b; // Повертає суму
}

int result = Add(5, 3); // result = 8

Використання для раннього виходу (Guard Clauses):

Guard Clauses (охоронні вирази) — це техніка програмування, яка полягає у використанні раннього повернення (early return) для перевірки умов та виходу з методу при помилкових або небажаних умовах. Ця практика покращує читабельність коду, зменшує вкладеність та робить логіку програми більш зрозумілою.

Принцип Guard Clauses

Guard Clauses діють за принципом "захисту" основної логіки методу від некоректних вхідних даних або станів. Вони розміщуються на початку методу та виконують перевірки, які дозволяють швидко вийти з методу, якщо умови не відповідають вимогам.

Переваги використання Guard Clauses

  1. Зменшення вкладеності - уникнення глибокого вкладання if-else блоків
  2. Покращення читабельності - основна логіка методу стає більш видимою
  3. Швидке виявлення помилок - проблеми виявляються на самому початку виконання
  4. Спрощення тестування - окремі умови легше тестувати незалежно
  5. Дотримання принципу "ранньої валідації" - перевірка вхідних даних перед початком обробки

Приклади Guard Clauses у C#

Приклад 1: Проста валідація параметрів

До (з вкладеними умовами):

void ProcessOrder(Order order)
{
    if (order != null)
    {
        if (order.Items.Count > 0)
        {
            if (order.Customer != null)
            {
                // Основна логіка обробки замовлення
                Console.WriteLine($"Обробка замовлення {order.Id} для клієнта {order.Customer.Name}");
                // ... складна логіка обробки
            }
            else
            {
                Console.WriteLine("Помилка: клієнт відсутній");
            }
        }
        else
        {
            Console.WriteLine("Помилка: замовлення порожнє");
        }
    }
    else
    {
        Console.WriteLine("Помилка: замовлення відсутнє");
    }
}

Після (з Guard Clauses):

void ProcessOrder(Order order)
{
    // Guard Clauses - перевірка умов на початку методу
    if (order == null)
    {
        Console.WriteLine("Помилка: замовлення відсутнє");
        return;
    }

    if (order.Items.Count == 0)
    {
        Console.WriteLine("Помилка: замовлення порожнє");
        return;
    }

    if (order.Customer == null)
    {
        Console.WriteLine("Помилка: клієнт відсутній");
        return;
    }

    // Основна логіка обробки замовлення
    Console.WriteLine($"Обробка замовлення {order.Id} для клієнта {order.Customer.Name}");
    // ... складна логіка обробки
}

Приклад 2: Використання бібліотеки Light.GuardClauses

Для більш елегантної реалізації Guard Clauses можна використовувати спеціалізовані бібліотеки, такі як Light.GuardClauses.

Встановлення бібліотеки:

dotnet add package Light.GuardClauses

Приклад з використанням бібліотеки:

using Light.GuardClauses;

public class OrderProcessor
{
    private readonly IOrderRepository _orderRepository;
    private readonly ICustomerService _customerService;

    public OrderProcessor(IOrderRepository orderRepository, ICustomerService customerService)
    {
        // Guard Clauses для конструктора
        _orderRepository = orderRepository.MustNotBeNull();
        _customerService = customerService.MustNotBeNull();
    }

    public void ProcessOrder(Order order)
    {
        // Guard Clauses для параметрів методу
        order.MustNotBeNull();
        order.Items.MustNotBeNullOrEmpty();
        order.Customer.MustNotBeNull();

        // Додаткові перевірки
        order.Id.MustNotBeEmpty();
        order.TotalAmount.MustBeGreaterThan(0);

        // Основна логіка обробки
        var customer = _customerService.GetCustomer(order.Customer.Id);
        customer.MustNotBeNull();

        _orderRepository.Save(order);
        Console.WriteLine($"Замовлення {order.Id} успішно оброблено");
    }
}

Приклад 3: Складні умови з використанням switch виразів

public string ValidateUserInput(string input, int minLength, int maxLength)
{
    // Guard Clauses з використанням switch виразів
    return input switch
    {
        null => "Вхідне значення не може бути null",
        { Length: 0 } => "Вхідне значення не може бути порожнім",
        string s when s.Length < minLength => $"Довжина повинна бути не менше {minLength}",
        string s when s.Length > maxLength => $"Довжина повинна бути не більше {maxLength}",
        string s when s.Trim().Length == 0 => "Вхідне значення не може містити тільки пробіли",
        _ => "Валідація пройшла успішно"
    };
}

Приклад 4: Guard Clauses у асинхронних методах

public async Task<OrderResult> ProcessOrderAsync(Order order, CancellationToken cancellationToken = default)
{
    // Guard Clauses для асинхронних методів
    if (order == null)
        throw new ArgumentNullException(nameof(order), "Замовлення не може бути null");

    if (order.Items == null || !order.Items.Any())
        throw new ArgumentException("Замовлення повинно містити хоча б один товар", nameof(order));

    if (cancellationToken.IsCancellationRequested)
        throw new OperationCanceledException("Операція була скасована");

    try
    {
        // Асинхронна обробка замовлення
        var validationResult = await ValidateOrderAsync(order, cancellationToken);
        if (!validationResult.IsValid)
        {
            return new OrderResult { Success = false, Error = validationResult.ErrorMessage };
        }

        var processedOrder = await SaveOrderAsync(order, cancellationToken);
        return new OrderResult { Success = true, OrderId = processedOrder.Id };
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Помилка під час обробки замовлення");
        throw;
    }
}

Правила використання Guard Clauses

Коли використовувати Guard Clauses:

  1. На початку методу - для перевірки вхідних параметрів
  2. Для валідації стану об'єкта - перед виконанням операцій
  3. Для перевірки авторизації - на початку методів, які вимагають прав доступу
  4. Для перевірки ресурсів - наявність файлів, з'єднань, конфігурацій

Коли НЕ використовувати Guard Clauses:

  1. Для складних обчислень - якщо перевірка вимагає значних витрат ресурсів
  2. Для необов'язкових перевірок - якщо помилка не критична
  3. У властивостях - може призвести до несподіваних виключень при доступі до властивостей

Найкращі практики:

  1. Розміщуйте Guard Clauses на початку методу
  2. Використовуйте зрозумілі повідомлення про помилки
  3. Групуйте пов'язані перевірки
  4. Використовуйте спеціалізовані бібліотеки для спрощення
  5. Дотримуйтесь єдиного стилю перевірок у всьому проекті

Візуальне порівняння підходів

Традиційний підхід (з вкладеними умовами):

Loading diagram...
flowchart TD
    Start([Початок]) --> Check1{order != null?}
    Check1 -->|Ні| Error1[Помилка: order null]
    Check1 -->|Так| Check2{Items.Count > 0?}
    Check2 -->|Ні| Error2[Помилка: порожнє]
    Check2 -->|Так| Check3{Customer != null?}
    Check3 -->|Ні| Error3[Помилка: Customer null]
    Check3 -->|Так| MainLogic[Основна логіка]

    Error1 --> End([Кінець])
    Error2 --> End
    Error3 --> End
    MainLogic --> End

    style Start fill:#22c55e,stroke:#16a34a,color:#ffffff
    style End fill:#64748b,stroke:#334155,color:#ffffff
    style Check1 fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Check2 fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Check3 fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Error1 fill:#ef4444,stroke:#b91c1c,color:#ffffff
    style Error2 fill:#ef4444,stroke:#b91c1c,color:#ffffff
    style Error3 fill:#ef4444,stroke:#b91c1c,color:#ffffff
    style MainLogic fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

Guard Clauses підхід (з раннім виходом):

Loading diagram...
flowchart TD
    Start([Початок]) --> Guard1{order == null?}
    Guard1 -->|Так| Error1[Помилка: order null]
    Guard1 -->|Ні| Guard2{Items.Count == 0?}
    Guard2 -->|Так| Error2[Помилка: порожнє]
    Guard2 -->|Ні| Guard3{Customer == null?}
    Guard3 -->|Так| Error3[Помилка: Customer null]
    Guard3 -->|Ні| MainLogic[Основна логіка]

    Error1 --> End([Кінець])
    Error2 --> End
    Error3 --> End
    MainLogic --> End

    style Start fill:#22c55e,stroke:#16a34a,color:#ffffff
    style End fill:#64748b,stroke:#334155,color:#ffffff
    style Guard1 fill:#10b981,stroke:#065f46,color:#ffffff
    style Guard2 fill:#10b981,stroke:#065f46,color:#ffffff
    style Guard3 fill:#10b981,stroke:#065f46,color:#ffffff
    style Error1 fill:#ef4444,stroke:#b91c1c,color:#ffffff
    style Error2 fill:#ef4444,stroke:#b91c1c,color:#ffffff
    style Error3 fill:#ef4444,stroke:#b91c1c,color:#ffffff
    style MainLogic fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

Текстове представлення структури:

ProcessOrder()
├── if (order != null)
│   └── if (order.Items.Count > 0)
│       └── if (order.Customer != null)
│           └── [Основна логіка]  ← Глибока вкладеність
│       └── else
│           └── [Помилка Customer]
│   └── else
│       └── [Помилка Items]
└── else
    └── [Помилка order]
ProcessOrder()
├── if (order == null) → return [Помилка order]
├── if (order.Items.Count == 0) → return [Помилка Items]
├── if (order.Customer == null) → return [Помилка Customer]
└── [Основна логіка]  ← Пряма лінія виконання

Порівняння підходів

АспектТрадиційний підхідGuard Clauses
ЧитабельністьНизька (глибока вкладеність)Висока (лінійна структура)
ПідтримкаСкладніше (багато рівнів вкладення)Простіше (очевидна структура)
ТестуванняСкладніше (потрібні складніші тести)Простіше (кожна умова окремо)
ПродуктивністьОднаковаОднакова
Обсяг кодуБільшеМенше

Висновки

Guard Clauses - це потужна техніка, яка допомагає писати чистіший, зрозуміліший та легший у супроводі код. Використовуючи цей підхід, ви:

  • Зменшуєте складність коду
  • Покращуєте його читабельність
  • Прискорюєте виявлення помилок
  • Спрощуєте процес тестування

Ця практика особливо корисна у великих проектах, де важлива підтримка коду та його розуміння командою розробників.

Ранній вихід (Early Return) з використанням Guard Clauses покращує читабельність, дозволяючи уникнути глибокого вкладання умовних операторів. Спочатку перевіряйте помилкові умови та виходьте, а потім обробляйте основну логіку.

Порівняння операторів переходу

break

Повністю виходить з циклу або switch. Виконання продовжується з наступного оператора після циклу.

for (int i = 0; i < 10; i++)
{
    if (i == 5) break;
    Console.Write(i + " ");
}
// Виведе: 0 1 2 3 4

continue

Пропускає решту коду в поточній ітерації, але цикл продовжується. Переходить безпосередньо до наступної ітерації.

for (int i = 0; i < 10; i++)
{
    if (i == 5) continue;
    Console.Write(i + " ");
}
// Виведе: 0 1 2 3 4 6 7 8 9

return

Виходить з усього методу, незалежно від циклів. Може повертати значення.

int FindFirst(int[] arr, int target)
{
    for (int i = 0; i < arr.Length; i++)
    {
        if (arr[i] == target)
        {
            return i; // Повертає індекс
        }
    }
    return -1; // Не знайдено
}

Зіставлення Шаблонів у Switch Виразах

Починаючи з C# 8.0, мова отримала потужний функціонал зіставлення шаблонів (pattern matching), який революціонізував роботу з умовною логікою. Switch вирази (switch expressions) — це сучасна альтернатива традиційним операторам switch, яка є більш виразною та компактною.

Еволюція: від оператора до виразу

string GetSeasonName(int month)
{
    string season;

    switch (month)
    {
        case 12:
        case 1:
        case 2:
            season = "Зима";
            break;
        case 3:
        case 4:
        case 5:
            season = "Весна";
            break;
        case 6:
        case 7:
        case 8:
            season = "Літо";
            break;
        case 9:
        case 10:
        case 11:
            season = "Осінь";
            break;
        default:
            season = "Невідомо";
            break;
    }

    return season;
}
Switch вираз — це компактна форма оператора switch, яка повертає значення. Основні відмінності:
  • Використовує => замість : та break
  • Значення записується справа від =>
  • Це вираз, а не оператор, тому повертає значення
  • Використовує _ (discard) замість default

Основні типи шаблонів

Константні шаблони (Constant Patterns)

Перевірка на точну відповідність значенню:

string GetDayType(int day) => day switch
{
    6 => "Субота",
    7 => "Неділя",
    _ => "Робочий день"
};

Реляційні шаблони (Relational Patterns)

Використання операторів порівняння <, >, <=, >=:

string GetWaterState(int temperature) => temperature switch
{
    < 0 => "Лід",
    0 => "Точка замерзання",
    > 0 and < 100 => "Рідина",
    100 => "Точка кипіння",
    > 100 => "Пара",
    _ => "Невідомий стан"
};

Логічні шаблони (Logical Patterns)

Комбінування шаблонів за допомогою and, or, not:

bool IsWeekday(int day) => day switch
{
    >= 1 and <= 5 => true,
    6 or 7 => false,
    _ => throw new ArgumentException("Некоректний день")
};

string ClassifyAge(int age) => age switch
{
    < 0 => "Некоректний вік",
    >= 0 and < 13 => "Дитина",
    >= 13 and < 18 => "Підліток",
    >= 18 and < 65 => "Дорослий",
    >= 65 => "Пенсіонер"
};

Шаблони властивостей (Property Patterns)

Перевірка властивостей об'єктів:

public record Person(string Name, int Age);

string GetCategory(Person person) => person switch
{
    { Age: < 18 } => "Неповнолітній",
    { Age: >= 18, Name: "Admin" } => "Адміністратор",
    { Age: >= 18 } => "Дорослий користувач",
    _ => "Невідомо"
};

Позиційні шаблони (Positional Patterns)

Використання деконструкції для перевірки кортежів або типів з деконструктором:

public record Point(int X, int Y);

string GetQuadrant(Point point) => point switch
{
    (0, 0) => "Початок координат",
    (> 0, > 0) => "Перший квадрант",
    (< 0, > 0) => "Другий квадрант",
    (< 0, < 0) => "Третій квадрант",
    (> 0, < 0) => "Четвертий квадрант",
    (_, 0) => "На осі X",
    (0, _) => "На осі Y"
};

Discard Pattern

Використання _ для ігнорування значень:

string GetInfo((string name, int age, string city) person) => person switch
{
    ("John", _, _) => "Це John, незалежно від віку та міста",
    (_, > 65, _) => "Пенсіонер",
    (_, _, "Kyiv") => "Житель Києва",
    _ => "Інший користувач"
};

Застереження (Case Guards) з when

Додаткові умови для більш точного зіставлення:

decimal CalculateDiscount(int age, bool isMember) => (age, isMember) switch
{
    ( < 18, _) => 0.20m, // 20% для дітей
    ( >= 65, _) => 0.15m, // 15% для пенсіонерів
    (_, true) when age >= 18 && age < 65 => 0.10m, // 10% для членів
    _ => 0m // Без знижки
};

Складніший приклад з when:

public record Order(int ItemCount, decimal TotalPrice, bool IsPriority);

string GetShippingMethod(Order order) => order switch
{
    { IsPriority: true } => "Експрес доставка",
    { ItemCount: > 10, TotalPrice: > 1000 } => "Вантажна доставка",
    { TotalPrice: > 500 } when order.ItemCount <= 5 => "Стандартна доставка з упаковкою",
    { ItemCount: <= 3 } => "Кур'єрська служба",
    _ => "Звичайна пошта"
};

Вичерпність (Exhaustiveness Checking)

Компілятор C# перевіряє, чи охоплюють шаблони всі можливі значення. Якщо ні, виникне попередження:

enum TrafficLight { Red, Yellow, Green }

// Попередження CS8509: Не всі значення охоплені
string GetAction(TrafficLight light) => light switch
{
    TrafficLight.Red => "Стоп",
    TrafficLight.Green => "Рух"
    // Відсутній Yellow!
};

// Правильна версія:
string GetActionCorrect(TrafficLight light) => light switch
{
    TrafficLight.Red => "Стоп",
    TrafficLight.Yellow => "Підготуватися",
    TrafficLight.Green => "Рух",
    _ => throw new ArgumentException("Невідомий сигнал")
};

Порівняння: традиційний switch vs switch вираз

АспектТрадиційний switchSwitch вираз
СинтаксисОператор з case, :, breakВираз з =>
Повернення значенняПотрібна окрема зміннаПряме повернення
КомпактністьБагатослівнийЛаконічний
Pattern matchingОбмежений (C# 7+)Повна підтримка
Перевірка вичерпностіНіТак (попередження)
Версія C#Всі версіїC# 8.0+

Практичний приклад: обробка HTTP статусів

public record HttpResponse(int StatusCode, string Body);

string HandleResponse(HttpResponse response) => response switch
{
    { StatusCode: 200, Body: not null } => $"Успіх: {response.Body}",
    { StatusCode: 200, Body: null } => "Успіх, але немає даних",
    { StatusCode: >= 200 and < 300 } => "Успішна відповідь",
    { StatusCode: 400 } => "Поганий запит",
    { StatusCode: 401 } => "Не авторизовано",
    { StatusCode: 404 } => "Не знайдено",
    { StatusCode: >= 400 and < 500 } => "Помилка клієнта",
    { StatusCode: >= 500 } => "Помилка сервера",
    _ => "Невідомий статус"
};
Switch вирази особливо корисні для створення чистого, декларативного коду. Вони ідеально підходять для:
  • Маппінгу значень (value mapping)
  • Класифікації даних
  • Обробки складних умов на основі множини властивостей
  • Редюсерів у state management
Loading diagram...
flowchart TD
    Start([Вхідне значення]) --> Switch{Switch вираз}
    Switch -->|Pattern 1 збігся| Result1[=> Результат 1]
    Switch -->|Pattern 2 збігся| Result2[=> Результат 2]
    Switch -->|Pattern 3 збігся| Result3[=> Результат 3]
    Switch -->|Pattern N збігся| ResultN[=> Результат N]
    Switch -->|Жоден не збігся| Discard[=> _ discard/default]

    Result1 --> End([Повернення значення])
    Result2 --> End
    Result3 --> End
    ResultN --> End
    Discard --> End

Найкращі Практики та Поширені Помилки

Найкращі практики

1. Уникайте глибокого вкладанняЗамість глибоко вкладених умов використовуйте ранній вихід:
void ProcessUser(User user)
{
    if (user != null)
    {
        if (user.IsActive)
        {
            if (user.HasPermission)
            {
                // Основна логіка
            }
        }
    }
}
2. Використовуйте правильний тип циклу
  • for — коли потрібен індекс або конкретна кількість ітерацій
  • foreach — для простої ітерації по колекції
  • while — коли кількість ітерацій невідома
  • do-while — коли потрібне мінімум одне виконання
3. Віддавайте перевагу switch виразамДля сучасного C# 8.0+ коду використовуйте switch вирази замість традиційних операторів switch, коли це доцільно.
4. Використовуйте осмислені імена змінних у циклах
foreach (var x in items)
{
    // Що таке x?
}

Поширені помилки

Нескінченні цикли
// Помилка: лічильник не змінюється
int i = 0;
while (i < 10)
{
    Console.WriteLine(i);
    // Забули i++
}
Вирішення: Завжди перевіряйте, що умова циклу колись стане хибною.
Зміна колекції під час ітерації
var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Помилка: зміна списку під час ітерації
foreach (var num in numbers)
{
    if (num % 2 == 0)
    {
        numbers.Remove(num); // InvalidOperationException!
    }
}
Вирішення: Використовуйте for в зворотному порядку або створіть нову колекцію.
// Правильно: ітерація у зворотному порядку
for (int i = numbers.Count - 1; i >= 0; i--)
{
    if (numbers[i] % 2 == 0)
    {
        numbers.RemoveAt(i);
    }
}
Відсутність break у switch
// Помилка компіляції в C#
switch (value)
{
    case 1:
        Console.WriteLine("One");
        // Забули break - помилка компіляції!
    case 2:
        Console.WriteLine("Two");
        break;
}
Вирішення: Завжди додавайте break, return або goto в кінці кожного case.
Неправильне використання continue та break
// Помилка: плутанина між continue та break
for (int i = 0; i < 10; i++)
{
    if (i == 5)
    {
        continue; // Пропускає лише 5
    }
    Console.WriteLine(i);
}
// Очікували вихід на 5, але отримали пропуск
Розуміння:
  • break — виходить з циклу
  • continue — пропускає поточну ітерацію

Troubleshooting (Усунення помилок)

Проблема 1: Condition is always true/false

Симптом: Попередження компілятора про завжди істинну або хибну умову.

int x = 5;
if (x = 5) // Помилка: присвоєння замість порівняння
{
    // ...
}

Вирішення:

int x = 5;
if (x == 5) // Правильно: оператор порівняння
{
    // ...
}

Проблема 2: Unreachable code detected

Симптом: Код після return, break або безумовного переходу ніколи не виконується.

int GetValue()
{
    return 42;
    Console.WriteLine("Це ніколи не виконається"); // Попередження
}

Вирішення: Видаліть недосяжний код або перегляньте логіку.

Проблема 3: Не всі шляхи повертають значення

Симптом: Компілятор повідомляє, що метод не завжди повертає значення.

int GetSign(int number)
{
    if (number > 0)
    {
        return 1;
    }
    else if (number < 0)
    {
        return -1;
    }
    // Що якщо number == 0?
}

Вирішення:

int GetSign(int number)
{
    if (number > 0) return 1;
    if (number < 0) return -1;
    return 0; // Обробка всіх випадків
}

Проблема 4: Off-by-one error у циклах

Симптом: Цикл виконується на одну ітерацію більше або менше, ніж очікувалось.

int[] arr = { 1, 2, 3, 4, 5 };

// Помилка: IndexOutOfRangeException
for (int i = 0; i <= arr.Length; i++) // Повинно бути <, а не <=
{
    Console.WriteLine(arr[i]);
}

Вирішення:

for (int i = 0; i < arr.Length; i++) // Правильно
{
    Console.WriteLine(arr[i]);
}

Практичні Завдання

Рівень 1: Базовий

Завдання 1.1: Парність числа

Напишіть програму, яка запитує у користувача ціле число та визначає, чи є воно парним або непарним, використовуючи оператор if-else.

Завдання 1.2: Сума чисел від 1 до N

Напишіть програму, яка обчислює суму всіх цілих чисел від 1 до N (включно) за допомогою циклу for.

Завдання 1.3: Виведення масиву

Створіть масив з 5 елементів та виведіть всі його елементи за допомогою циклу foreach.

Рівень 2: Середній

Завдання 2.1: Калькулятор з switch

Створіть простий калькулятор, який приймає два числа та оператор (+, -, *, /) і виконує відповідну операцію, використовуючи switch вираз.

Завдання 2.2: Таблиця множення

Напишіть програму, яка виводить таблицю множення для числа від 1 до 10, використовуючи вкладені цикли for.

Завдання 2.3: Пошук простих чисел

Напишіть програму, яка знаходить всі прості числа від 2 до N. Використовуйте оператори continue або break для оптимізації.

Рівень 3: Просунутий

Завдання 3.1: Pattern Matching для класифікації фігур

Створіть ієрархію класів для геометричних фігур (коло, прямокутник, трикутник) та напишіть метод, який використовує pattern matching для обчислення площі.

Завдання 3.2: Оптимізація пошуку в масиві

Напишіть метод бінарного пошуку в відсортованому масиві. Використовуйте цикл while та оператори переходу для ефективного пошуку.

Завдання 3.3: Складний switch вираз для обробки HTTP запитів

Створіть систему обробки HTTP запитів з використанням pattern matching для маршрутизації та обробки різних типів запитів.


Резюме

У цьому розділі ви вивчили потік керування (control flow) — фундаментальні механізми, які дозволяють програмам приймати рішення та повторювати дії:

Умовні оператори:

  • if, else, else if для базових умовних розгалужень
  • Тернарний оператор ? : для компактних умовних виразів
  • Оператор switch для множинного вибору
  • Сучасні switch вирази з pattern matching (C# 8.0+)

Цикли:

  • for — для фіксованої кількості ітерацій з доступом до індексу
  • foreach — для зручної ітерації по колекціях
  • while — для циклів з попередньою перевіркою умови
  • do-while — для циклів з гарантованим першим виконанням

Оператори переходу:

  • break — вихід з циклу або switch
  • continue — пропуск поточної ітерації циклу
  • return — вихід з методу з можливим поверненням значення

Pattern Matching:

  • Константні, реляційні, логічні шаблони
  • Шаблони властивостей та позиційні шаблони
  • Case guards з when для додаткових умов
  • Перевірка вичерпності компілятором

Опанування потоку керування — це ключ до написання ефективної та елегантної логіки програми. Практикуйте різні підходи, обирайте найбільш читабельні рішення та використовуйте сучасні можливості C# для створення чистого коду!