1. Вступ
Чиста функція — це функція, яка за однакових вхідних даних завжди повертає однаковий результат і не спричиняє жодних побічних ефектів. Якщо простіше: чиста функція нічого не змінює довкола себе й не залежить від неконтрольованих зовнішніх впливів.
Функція вважається чистою, якщо:
- Вона лише обчислює значення на основі вхідних аргументів.
- Не змінює зовнішніх змінних або стан програми.
- Не залежить від зовнішніх змінних чи станів, які можуть змінитися.
Два золоті правила чистоти
- Детермінованість: той самий вхід — той самий вихід.
- Жодних побічних ефектів: функція не змінює нічого поза собою — ні файлів, ні глобальних змінних, ні інтерфейсу користувача (зокрема й виведення на екран через Console.WriteLine).
Не релігія, а здоровий глузд
Може здатися, що це суто академічна вимога, та практика доводить протилежне:
- Чиста функція передбачувана. Її легко тестувати: ви викликаєте її з певними аргументами й отримуєте конкретний результат.
- Чисту функцію можна вільно комбінувати у коді, не боячись, що десь щось зламається.
- У багатоядерному світі чиста функція — це гарантія безпеки: її можна запускати паралельно без ризику гонок даних.
2. Приклади чистих і нечистих функцій
Чиста функція: усе передбачувано
// Чиста функція: не чіпає нічого поза собою
int Add(int a, int b)
{
return a + b;
}
int Square(int x)
{
return x * x;
}
Викликайте Add(2, 3) хоч сто разів поспіль — завжди буде 5. Нудно, але надійно.
Нечиста функція: порушує правила
// Порушує чистоту: залежить від зовнішнього стану (статичної змінної)
int counter = 0;
int Increase()
{
counter++;
return counter;
}
Тут виклик Increase() щоразу повертає нове значення — тобто порушується детермінованість.
// Порушує чистоту: викликає зовнішній ефект (виведення на екран)
int AddAndPrint(int a, int b)
{
int sum = a + b;
Console.WriteLine(sum); // Побічний ефект!
return sum;
}
А як щодо випадковості та часу?
Будь-яка функція, що використовує DateTime.Now або Random, уже не чиста:
// Нечиста!
int GetRandomNumber()
{
return new Random().Next();
}
Таблиця відмінностей
| Особливість | Чиста функція | Нечиста функція |
|---|---|---|
| Завжди однаковий результат для однакових аргументів | Так | Ні |
| Побічні ефекти | Ні | Так |
| Залежність від зовнішнього стану | Ні | Так |
3. Незмінність даних: теорія та практика
Незмінність (immutability) — це підхід, коли об’єкт не можна змінити після створення. Якщо потрібно інше значення — створюємо новий об’єкт.
Чому це важливо?
- Застосунок стає стійким до випадкових помилок зміни даних.
- Немає потайних «витоків» змін: якщо у вас є об’єкт, ніхто нишком не змінить у ньому поле.
- Незмінність — основа для багатьох автоматичних оптимізацій і паралельних обчислень.
Прості приклади в C#
Незмінні типи в .NET
Рядки (string) у C# — незмінні. Коли ви викликаєте string.Concat(s, "world"), створюється новий рядок.
string s = "Hello";
string t = s;
s = s + " World";
Console.WriteLine(t); // t == "Hello"
Масиви й колекції: змінні за замовчуванням
int[] numbers = { 1, 2, 3 };
numbers[0] = 42; // Масив змінився!
Незмінність «на пальцях»: реалізація в коді
Замість зміни наявного об’єкта/значення повертаємо новий:
// Замість цього:
void AddToList(List<int> list, int value)
{
list.Add(value); // Змінює стан!
}
// Краще так:
List<int> AddToList(List<int> list, int value)
{
var newList = new List<int>(list) { value }; // Новий список
return newList;
}
Ілюстрація: мутація та незмінність
flowchart LR
A[Початковий об’єкт] --"мутація"--> B[Той самий об’єкт, але інший всередині]
A --"незмінність"--> C[Новий об’єкт]
4. Навіщо це потрібно в реальному C#‑коді?
- У сучасному C# бібліотеки на кшталт LINQ, Entity Framework та ASP.NET Core роблять ставку на чисті функції й незмінність.
- Незмінність зменшує кількість «магічних» багів, коли хтось десь загубив важливе значення.
- Чисті функції дають змогу легко використовувати автоматичне тестування (unit‑тести), адже для тесту треба просто перевірити вхід і вихід, не переймаючись зовнішнім світом.
Приклад: робота з рядками
string s = "Hello";
string newS = s.Replace("H", "J"); // s як і раніше "Hello"; newS — "Jello"
Приклад: LINQ і колекції
Where, Select та інші методи повертають нові колекції, не чіпаючи старі.
var numbers = new List<int> { 1, 2, 3, 4 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// numbers залишився незмінним!
Приклад із реального життя: конфігурація через незмінні об’єкти
Багато сучасних API .NET використовують незмінні об’єкти для конфігурації, наприклад JsonSerializerOptions:
var options = new JsonSerializerOptions
{
WriteIndented = true
};
// Цей об’єкт не змінюється «по ходу п’єси», що підвищує надійність.
5. Типові помилки та пастки
Слизька доріжка починається тоді, коли ви «випадково» модифікуєте дані в нібито «чистому» коді.
Часто так буває з колекціями: хотіли відфільтрувати список, а в процесі змінили вихідний.
Або ви забули, що метод на кшталт List<T>.Add змінює об’єкт на місці.
Підступний приклад:
List<int> DoubleTheNumbers(List<int> xs)
{
// Помилка! Змінюємо вихідний список, повертаємо той самий об’єкт.
foreach (var i in xs)
xs.Add(i * 2);
return xs;
}
Цей код навіть призведе до помилки виконання (InvalidOperationException), бо ви змінюєте колекцію під час її обходу — класична проблема мутації.
Правильно:
List<int> DoubleTheNumbers(List<int> xs)
{
var newList = new List<int>(xs.Select(x => x * 2));
return newList;
}
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ