Поделиться через


Создание типов записей

записи — это типы, использующие равенство на основе значений. Записи можно определить как ссылочные типы или типы значений. Две переменные типа записи равны, если определения типов записей идентичны, а если для каждого поля значения в обеих записях равны. Две переменные типа класса равны, если объекты, на которые ссылается, являются одинаковыми типами классов, а переменные ссылаются на один и тот же объект. Равенство, основанное на значениях, подразумевает другие возможности, которые вы, вероятно, хотите видеть в типах записей. Компилятор создает многие из этих элементов при объявлении record вместо class. Компилятор создает те же методы для типов record struct.

В этом руководстве описано, как:

  • Определите, добавляете ли модификатор record в тип class.
  • Объявите типы записей и типы позиционных записей.
  • Замените создаваемые компилятором методы вашими методами в записях.

Необходимые условия

Необходимо настроить компьютер для запуска .NET 6 или более поздней версии. Компилятор C# доступен с Visual Studio 2022 или пакета SDK для .NET.

Характеристики записей

Вы определяете запись , объявляя тип с помощью ключевого слова record или изменяя объявления class или struct. При необходимости можно опустить ключевое слово class для создания record class. Запись соответствует семантике равенства, основанной на значениях. Чтобы применить семантику значений, компилятор создает несколько методов для типа записи (как для типов record class, так и для типов record struct):

  • Переопределение Object.Equals(Object).
  • Виртуальный Equals метод, параметр которого является типом записи.
  • Переопределение Object.GetHashCode().
  • Методы для operator == и operator !=.
  • Типы записей реализуют System.IEquatable<T>.

Записи также позволяют изменить настройки Object.ToString(). Компилятор синтезирует методы для отображения записей с помощью Object.ToString(). Вы изучите эти члены при написании кода для этого руководства. Записи поддерживают выражения with для обеспечения неразрушающей мутации записей.

Вы также можете объявлять позиционные записи, используя более краткий синтаксис. Компилятор синтезирует дополнительные методы при объявлении позиционных записей:

  • Основной конструктор, параметры которого соответствуют позиционным параметрам объявления записи.
  • Общедоступные свойства для каждого параметра первичного конструктора. Эти свойства являются только для record class типов и типов readonly record struct. Для типов record struct предусмотрены функции -чтения и записи.
  • Метод Deconstruct для извлечения свойств из записи.

Создание данных температуры

Данные и статистика являются одними из сценариев, в которых требуется использовать записи. В этом руководстве вы создадите приложение, которое вычисляет градусо-дни для различных целей. Градусо-дни — это мера тепла (или его отсутствия) за период дней, недель или месяцев. Градусные дни отслеживают и прогнозируют потребление энергии. Более горячие дни означают больше кондиционирования воздуха, а более холодные дни означают больше использования печей. Тепловые дни помогают управлять популяцией растений и ростом растений по мере смены сезонов. Дни градусов помогают отслеживать миграцию животных для видов, которые путешествуют в соответствии с климатом.

Формула основана на средней температуре в течение заданного дня и базовой температуры. Для вычисления градусо-дней вам потребуется знать максимальную и минимальную температуру за каждый день в течение определенного периода. Начнем с создания нового приложения. Создайте консольное приложение. Создайте новый тип записи в новом файле с именем "DailyTemperature.cs":

public readonly record struct DailyTemperature(double HighTemp, double LowTemp);

Предыдущий код определяет позиционной записи. Запись DailyTemperature является readonly record struct, потому что вы не намерены наследовать от неё, и она должна быть неизменяемой. Свойства HighTemp и LowTemp являются свойствами инициализации только, то есть их можно задать в конструкторе или с помощью инициализатора свойств. Если вы хотите, чтобы позиционные параметры можно было читать и записывать, объявите record struct вместо readonly record struct. Тип DailyTemperature также имеет основной конструктор с двумя параметрами, соответствующими двум свойствам. Вы используете основной конструктор для инициализации записи DailyTemperature. Следующий код создает и инициализирует несколько DailyTemperature записей. Первый использует именованные параметры для уточнения HighTemp и LowTemp. Остальные инициализаторы используют позиционные параметры для инициализации HighTemp и LowTemp:

private static DailyTemperature[] data = [
    new DailyTemperature(HighTemp: 57, LowTemp: 30), 
    new DailyTemperature(60, 35),
    new DailyTemperature(63, 33),
    new DailyTemperature(68, 29),
    new DailyTemperature(72, 47),
    new DailyTemperature(75, 55),
    new DailyTemperature(77, 55),
    new DailyTemperature(72, 58),
    new DailyTemperature(70, 47),
    new DailyTemperature(77, 59),
    new DailyTemperature(85, 65),
    new DailyTemperature(87, 65),
    new DailyTemperature(85, 72),
    new DailyTemperature(83, 68),
    new DailyTemperature(77, 65),
    new DailyTemperature(72, 58),
    new DailyTemperature(77, 55),
    new DailyTemperature(76, 53),
    new DailyTemperature(80, 60),
    new DailyTemperature(85, 66) 
];

Вы можете добавить собственные свойства или методы в записи, включая позиционные записи. Для каждого дня необходимо вычислить средную температуру. Это свойство можно добавить в запись DailyTemperature:

public readonly record struct DailyTemperature(double HighTemp, double LowTemp)
{
    public double Mean => (HighTemp + LowTemp) / 2.0;
}

Давайте убедимся, что вы можете использовать эти данные. Добавьте следующий код в метод Main:

foreach (var item in data)
    Console.WriteLine(item);

Запустите приложение, и вы увидите выходные данные, похожие на следующий дисплей (несколько строк, удаленных для пространства):

DailyTemperature { HighTemp = 57, LowTemp = 30, Mean = 43.5 }
DailyTemperature { HighTemp = 60, LowTemp = 35, Mean = 47.5 }


DailyTemperature { HighTemp = 80, LowTemp = 60, Mean = 70 }
DailyTemperature { HighTemp = 85, LowTemp = 66, Mean = 75.5 }

В приведенном выше коде показан результат переопределения ToString, который синтезирован компилятором. Если вы предпочитаете другой текст, вы можете написать собственную версию ToString, которая запрещает компилятору синтезировать версию.

Вычисление градусо-дней

Для вычисления градусо-дней вычисляют разницу между базовой температурой и средней температурой за конкретный день. Чтобы измерить тепло с течением времени, вы сбрасываете все дни, когда средняя температура ниже базовой. Чтобы измерить холод с течением времени, вы исключите любые дни, когда средняя температура превышает базовый уровень. Например, в США используют 18°C в качестве базовой температуры для расчета градусо-дней отопления и охлаждения. Это температура, где не требуется нагревание или охлаждение. Если день имеет среднюю температуру 70 °F, этот день составляет пять градусов-дней охлаждения и ноль градусов-дней нагрева. И наоборот, если средняя температура составляет 12,8 °C, этот день считается 10 градусо-днями отопления и 0 градусо-днями охлаждения.

Эти формулы можно представить как небольшую иерархию типов записей: абстрактный тип градусо-дней и два конкретных типа для градусов-дней отопления и градусов-дней охлаждения. Эти типы также могут быть позициональными записями. Они принимают базовую температуру и последовательность ежедневных записей температуры в качестве аргументов основного конструктора:

public abstract record DegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords);

public sealed record HeatingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean < BaseTemperature).Sum(s => BaseTemperature - s.Mean);
}

public sealed record CoolingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean > BaseTemperature).Sum(s => s.Mean - BaseTemperature);
}

Абстрактная DegreeDays запись — это общий базовый класс для HeatingDegreeDays и CoolingDegreeDays записей. Объявления основного конструктора производных записей показывают, как управлять инициализацией базовой записи. Ваша производная запись определяет параметры для всех параметров в основном конструкторе базовой записи. Базовая запись объявляет и инициализирует эти свойства. Производная запись не скрывает их, но создает и инициализирует свойства для параметров, которые не объявлены в базовой записи. В этом примере производные записи не добавляют новые параметры первичного конструктора. Протестируйте код, добавив следующий код в метод Main:

var heatingDegreeDays = new HeatingDegreeDays(65, data);
Console.WriteLine(heatingDegreeDays);

var coolingDegreeDays = new CoolingDegreeDays(65, data);
Console.WriteLine(coolingDegreeDays);

Вы получите выходные данные, как показано ниже.

HeatingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 85 }
CoolingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 71.5 }

Определение синтезированных компилятором методов

Код вычисляет правильное количество дней нагрева и охлаждения за этот период времени. Но в этом примере показано, почему может потребоваться заменить некоторые синтезированные методы для записей. Вы можете объявить собственную версию любого из синтезированных компилятором методов в типе записи, за исключением метода клонирования. Метод клонирования имеет имя, созданное компилятором, и вы не можете предоставить другую реализацию. Эти синтезированные методы включают в себя конструктор копирования, элементы интерфейса System.IEquatable<T>, тесты на равенство и неравенство, а также GetHashCode(). Для этого синтезируется PrintMembers. Вы также можете объявить своё ToString, но PrintMembers представляет собой лучший вариант для сценариев наследования. Чтобы предоставить собственную версию синтезированного метода, сигнатура должна соответствовать синтезированному методу.

Элемент TempRecords в выходных данных консоли не полезен. Он отображает тип, но ничего другого. Это поведение можно изменить, предоставив собственную реализацию синтезированного PrintMembers метода. Подпись зависит от модификаторов, применяемых к объявлению record:

  • Если тип записи sealedили record struct, то подпись — private bool PrintMembers(StringBuilder builder);
  • Если тип записи не sealed и является производным от object (т. е. не объявляет базовую запись), сигнатура protected virtual bool PrintMembers(StringBuilder builder);
  • Если тип записи не sealed и является производным от другого типа, то сигнатура будет protected override bool PrintMembers(StringBuilder builder);.

Эти правила проще всего понять с помощью понимания цели PrintMembers. PrintMembers добавляет сведения о каждом свойстве в типе записи в строку. Контракт требует, чтобы базовые записи добавляли свои элементы к отображению и предполагает, что производные записи добавляют свои элементы. Каждый тип записи синтезирует переопределяющую запись ToString, похожую на следующий пример для HeatingDegreeDays:

public override string ToString()
{
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.Append("HeatingDegreeDays");
    stringBuilder.Append(" { ");
    if (PrintMembers(stringBuilder))
    {
        stringBuilder.Append(" ");
    }
    stringBuilder.Append("}");
    return stringBuilder.ToString();
}

Вы объявляете метод PrintMembers в записи DegreeDays, который не отображает тип коллекции.

protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
    stringBuilder.Append($"BaseTemperature = {BaseTemperature}");
    return true;
}

Подпись объявляет метод virtual protected для сопоставления версии компилятора. Не беспокойтесь, если вы неправильно используете методы доступа; язык применяет правильную сигнатуру. Если вы забыли правильные модификаторы для любого синтезированного метода, компилятор выдает предупреждения или ошибки, которые помогают получить правильную подпись.

В типе записи метод ToString можно объявить в качестве sealed. Это предотвращает разработку новой реализации в производных записях. Производные записи по-прежнему содержат переопределение PrintMembers. Вы запечатываете ToString, если вы не хотите, чтобы он отображал тип среды выполнения записи. В предыдущем примере вы потеряете информацию о том, где запись измеряла дни отопления или охлаждения.

Недеструктивная мутация

Синтезированные элементы в классе позиционной записи не изменяют состояние записи. Цель заключается в том, что вы можете проще создавать неизменяемые записи. Помните, что вы объявляете readonly record struct для создания неизменяемой структуры записи. Просмотрите предыдущие объявления для HeatingDegreeDays и CoolingDegreeDays. Добавленные члены выполняют вычисления по значениям записи, но не изменяют состояние. Позиционные записи упрощают создание неизменяемых ссылочных типов.

Создание неизменяемых ссылочных типов означает, что вы хотите использовать недеструктивную мутацию. Вы создаете новые экземпляры записей, аналогичные существующим экземплярам записей, с помощью выражений with. Эти выражения являются конструкцией копирования с дополнительными операциями присваивания, которые изменяют копию. Результатом является новый экземпляр записи, в котором каждое свойство было скопировано из существующей записи и при необходимости изменено. Исходная запись не изменяется.

Давайте добавим в программу несколько функций, демонстрирующих with выражения. Сначала создадим новую запись для вычисления градусо-дней роста с использованием тех же данных. дней растущей степени обычно использует 41 F в качестве базовой и измеряет температуру выше базового уровня. Чтобы использовать те же данные, можно создать новую запись, аналогичную coolingDegreeDays, но с другой базовой температурой:

// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);

Можно сравнить количество градусов, вычисляемых с числами, созданными с более высокой базовой температурой. Помните, что записи ссылочных типов, и эти копии являются неглубокими копиями. Массив данных не копируется, но обе записи ссылаются на одни и те же данные. Этот факт является преимуществом в одном другом сценарии. Чтобы отслеживать дни активных температур, полезно учитывать их сумму за предыдущие пять дней. Вы можете создавать новые записи с различными исходными данными с помощью выражений with. Следующий код создает коллекцию этих накоплений, а затем отображает значения:

// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
    var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
    movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
    Console.WriteLine(item);
}

Для создания копий записей можно также использовать выражения with. Не указывайте свойства между фигурными скобками для выражения with. Это означает, что создайте копию и не изменяйте какие-либо свойства:

var growingDegreeDaysCopy = growingDegreeDays with { };

Запустите готовое приложение, чтобы просмотреть результаты.

Сводка

В этом руководстве показано несколько аспектов записей. Записи предоставляют краткий синтаксис для типов, в которых основное использование хранит данные. Для объектно-ориентированных классов основное использование определяет обязанности. Это руководство посвящено позиционным записям, где можно использовать краткий синтаксис для объявления свойств записи. Компилятор синтезирует несколько элементов записи для копирования и сравнения записей. Вы можете добавить любых других участников, которые вам нужны для типов записей. Вы можете создавать неизменяемые типы записей, зная, что ни один из созданных компилятором элементов не изменял состояние. И with выражения позволяют легко поддерживать недеструктивную мутацию.

Записи добавляют еще один способ определения типов. Определения class используются для создания объектно-ориентированных иерархий, ориентированных на обязанности и поведение объектов. Вы создаете struct типы для структур данных, предназначенных для хранения данных и достаточно малых для эффективного копирования. Вы создаете record типы, если требуется равенство на основе значений и сравнение, не хотите копировать значения и использовать ссылочные переменные. Вы создаете record struct типы, если вам нужны характеристики записей для типа, который достаточно небольшой, чтобы эффективно копироваться.

Дополнительные сведения о записях см. в справочной статье по языку C# для типа записи , а также в спецификации предложенного типа записи и спецификации структуры записи .