Что такое иммутабельность и как реализовать неизменяемость в классе с полями и свойствами?
Что проверяют
Умеете ли вы не просто дать определение, а закрыть все щели, через которые состояние всё-таки меняется. Вопрос сформулирован про поля и свойства не случайно: интервьюер ждёт конкретных языковых средств, а не рассуждения о пользе неизменяемости.
Как отвечать
Объект неизменяем, если после выхода из конструктора его наблюдаемое состояние не меняется ни одним публичным способом, а изменение выражается созданием нового объекта.
Рецепт по пунктам. Поля объявляются как readonly и заполняются только в конструкторе. Свойства идут без сеттера либо с аксессором init, если объект удобно собирать инициализатором. Класс помечается sealed, иначе наследник добавит изменяемое состояние. Ссылочные аргументы копируются на входе, а наружу отдаётся неизменяемая коллекция.
public sealed class Order
{
public string Number { get; }
public ImmutableArray<string> Items { get; }
public Order(string number, IEnumerable<string> items)
{
Number = number;
Items = items.ToImmutableArray();
}
public Order WithNumber(string number) => new(number, Items);
}Короткий путь это позиционная запись record: компилятор сам сделает свойства только для чтения, сравнение по значению и выражение with для копии с изменением. Оговорка обязательна: глубокой неизменяемости record не даёт.
Где спотыкаются
Ставят readonly на поле со списком и считают дело сделанным. Модификатор запрещает переприсвоить ссылку, а добавление элемента по этой ссылке работает как ни в чём не бывало.
Возвращают наружу внутреннюю коллекцию. Вызывающий код меняет её у себя, а меняется состояние объекта. Отдавать надо копию или по-настоящему неизменяемый тип: возврат под интерфейсом только для чтения защищает от случайности, но обратное приведение к списку его обходит.
Путают with с глубоким клонированием. Копия поверхностная, вложенные ссылочные объекты остаются общими у старого и нового экземпляра, и правка через новый незаметно меняет старый.