Неожиданная Несогласованность в Записях
На днях Джон Скит пытался найти ошибку в своём коде, и она оказалась следствием его непонимания принципов работы записей в C#. Как показывает вчерашний опрос, он не единственный, кто ожидал, что они будут работать именно так.
Когда записи появились в C#, одновременно появился и оператор «обратимого изменения» with. Идея заключается в том, что типы записей неизменяемы, но вы можете легко и эффективно создать новый экземпляр с теми же данными, что и существующий экземпляр, но с другими значениями некоторых свойств:
public record HighScoreEntry(
string PlayerName, int Score, int Level);
HighScoreEntry entry = new("Jon", 5000, 50);
var updatedEntry =
entry with { Score = 6000, Level = 55 };
Это не изменяет данные первоначальной записи, т.е. entry.Score останется 5000.
Проблема
В качестве очень простого (и весьма надуманного) примера можно создать запись, которая определяет, является ли значение чётным или нечётным при инициализации:
public record Number(int Value)
{
public bool Even { get; } =
(Value & 1) == 0;
}
На первый взгляд, всё нормально:
var n2 = new Number(2);
var n3 = new Number(3);
Console.WriteLine(n2);
// Number { Value = 2, Even = True }
Console.WriteLine(n3);
// Number { Value = 3, Even = False }
Но если мы изменим код на использование with:
var n3 = n2 with { Value = 3 };
Console.WriteLine(n3);
// Number { Value = 3, Even = True }Джон (и не только он) всегда предполагал, что оператор with вызывает конструктор с новыми значениями. На самом деле это не так. Оператор with выше преобразуется в примерно такой код:
var n3 = n2.<Clone>$();
n3.Value = 3;
Метод <Clone>$ (по крайней мере, в этом случае) вызывает сгенерированный конструктор копирования (Number(Number)), который копирует как Value, так и резервное поле для Even (т.е. значение Even). Всё это документировано, но пока компилятор не выдаёт никаких предупреждений о возможных несоответствиях, которые это может вызвать.
Конечно, вычисляемое свойство работает правильно. Значение вычисляется каждый раз при обращении к нему:
public record Number(int Value)
{
public bool Even => (Value & 1) == 0;
}
Что делать?
Пока просто знать об этом и жить дальше. Легко сказать, что можно просто использовать вычисляемые свойства. Проблема, если кто-то, не знающий об этом, добавит/изменит свойство на вычисляемое при инициализации. Либо если запись предоставлена в какой-то внешней библиотеке (не заглядывая в исходный код библиотеки, нет возможности определить какое свойство там использовано). Сложно представить, что это поведение будет изменено в языке. Но Джон попросил Майкрософт хотя бы добавить предупреждение в таких случаях, а также написал собственный анализатор.
Источник: https://codeblog.jonskeet.uk/2025/07/19/unexpected-inconsistency-in-records/