Значения и неизменяемость: let и иммутабельность по умолчанию
Главный сдвиг мышления: в F# значения неизменяемы по умолчанию, а не переменны.
Неизменяемость означает, что однажды связанное значение нельзя поменять;
let x = 5привязывает имя к значению, а не объявляет ячейку для перезаписи.
let привязывает, а не присваивает
В императивных языках x = 5 — это присваивание ячейке, которую потом можно перезаписать. В F# let x = 5 — это связывание (binding): имя x навсегда означает 5 в своей области видимости.
let x = 5
// x <- 6 // ОШИБКА: значение неизменяемоПопытка изменить x не скомпилируется. Это не ограничение, а гарантия: вы точно знаете, что значение под именем не «уплывёт» из-под ног в другой части программы.
Зачем неизменяемость
Изменяемое состояние — источник большинства коварных багов: что-то меняет значение, и поведение программы зависит от порядка и времени изменений. Неизменяемые данные:
- Предсказуемы — значение не меняется, рассуждать о коде проще.
- Безопасны для потоков — нечего «защищать» от гонок, можно свободно делить между потоками.
- Удобны для отладки — состояние не «портится» в неожиданный момент.
Когда нужна мутация: mutable
F# прагматичен: если мутация действительно нужна (счётчик в цикле, буфер), явно объявите mutable и присваивайте оператором <-.
let mutable counter = 0
counter <- counter + 1
counter <- counter + 1
printfn "%d" counterВывод:
2
Ключевое слово mutable делает мутацию видимой в коде: читатель сразу понимает, что здесь состояние меняется. По умолчанию же таких сюрпризов нет.
Затенение (shadowing)
Иногда хочется «переиспользовать» имя, не нарушая неизменяемость. F# позволяет затенить прежнюю привязку новой с тем же именем.
let value = 10
let value = value + 5 // новая привязка, прежняя value = 10 затенена
printfn "%d" valueВывод:
15
Это не мутация: создаётся новое неизменяемое значение, а старое перестаёт быть видимым. Тип при этом может даже поменяться.
Как работает под капотом
Неизменяемое связывание компилируется в обычное поле/локальную переменную IL, которая просто не перезаписывается — никаких накладных расходов в рантайме. mutable-значения становятся изменяемыми ячейками. Затенение — это две независимые привязки на уровне компилятора, во второй области видимости имя ссылается на новое значение; старое существует, но недоступно по имени.
Частые ошибки
- Пытаться писать
x <- ...для обычногоlet— нужноmutable. - Путать затенение с мутацией: затенение создаёт новое значение, не меняя старое.
- Злоупотреблять
mutableпо привычке из C# — обычно есть функциональная альтернатива (рекурсия, fold).
Итоги
letсоздаёт неизменяемое связывание имени со значением.- Неизменяемость по умолчанию делает код предсказуемым и потокобезопасным.
- Для мутации нужны
mutableи оператор<-— она становится явной. - Затенение переиспользует имя через новую привязку, не меняя старое значение.