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"

Массивы и коллекции: mutable by default

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;
}
2
Задача
C# SELF, 51 уровень, 1 лекция
Недоступна
Неизменяемый список чисел
Неизменяемый список чисел
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ