JavaRush /Курси /C# SELF /Чисті функції та незмінність

Чисті функції та незмінність

C# SELF
Рівень 51 , Лекція 1
Відкрита

1. Вступ

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

Функція вважається чистою, якщо:

  • Вона лише обчислює значення на основі вхідних аргументів.
  • Не змінює зовнішніх змінних або стан програми.
  • Не залежить від зовнішніх змінних чи станів, які можуть змінитися.

Два золоті правила чистоти

  1. Детермінованість: той самий вхід — той самий вихід.
  2. Жодних побічних ефектів: функція не змінює нічого поза собою — ні файлів, ні глобальних змінних, ні інтерфейсу користувача (зокрема й виведення на екран через 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;
}
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ