Записи (справочник по C#)
Модификатор используется record
для определения ссылочного типа , который предоставляет встроенные функции для инкапсулирования данных. C# 10 позволяет record class
синтаксису в качестве синонима уточнить ссылочный тип и record struct
определить тип значения с аналогичной функциональностью.
При объявлении первичного конструктора в записи компилятор создает общедоступные свойства для параметров первичного конструктора. Основные параметры конструктора записи называются позициальными параметрами. Компилятор создает позиционные свойства , которые отражают основной конструктор или позиционные параметры. Компилятор не синтезирует свойства для параметров первичного конструктора для типов, не имеющих record
модификатора.
В следующих двух примерах демонстрируются record
ссылочные типы:record class
public record Person(string FirstName, string LastName);
public record Person
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
};
В следующих двух примерах показаны record struct
типы значений:
public readonly record struct Point(double X, double Y, double Z);
public record struct Point
{
public double X { get; init; }
public double Y { get; init; }
public double Z { get; init; }
}
Вы также можете создавать записи с изменяемыми свойствами и полями:
public record Person
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
};
Структуры записей также могут быть изменяемыми, как позиционированные структуры записей, так и структуры записей без параметров позиционирования:
public record struct DataMeasurement(DateTime TakenAt, double Measurement);
public record struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
Несмотря на поддержку изменения, записи предназначены в первую очередь для неизменяемых моделей данных. Тип записи предоставляет следующие возможности:
- Краткий синтаксис для создания ссылочного типа с неизменяемыми свойствами.
- Встроенное поведение, полезное для ссылочного типа, ориентированного на данные:
- Поддержка иерархий наследования.
В предыдущих примерах показаны некоторые различия между записями, которые являются ссылочными типами, и записями, которые являются типами значений:
- Объект
record
илиrecord class
объявляет ссылочный тип. Ключевое словоclass
является необязательным, но может добавить ясность для читателей.record struct
объявляет тип значения. - Свойства позиционирования являются неизменяемыми в
record class
иreadonly record struct
. Они являются изменяемыми вrecord struct
.
В оставшейся части этой статьи обсуждаются типы record class
и record struct
. Различия подробно описаны в каждом разделе. Выберите между record class
и record struct
, как вы выбираете между class
и struct
. Термин запись используется для описания поведения, которое применяется ко всем типам записей. record struct
или record class
используется для описания поведения, которое применяется только к типам структур или классов соответственно. Тип record struct
появился в C# 10.
Позиционный синтаксис для определения свойств
Позиционные параметры позволяют объявить свойства записи и инициализировать значения свойств при создании экземпляра:
public record Person(string FirstName, string LastName);
public static void Main()
{
Person person = new("Nancy", "Davolio");
Console.WriteLine(person);
// output: Person { FirstName = Nancy, LastName = Davolio }
}
При использовании позиционного синтаксиса для определения свойства компилятор создает следующие элементы:
- Общедоступное автоматически реализованное свойство для каждого позиционного параметра, предоставленного в объявлении записи.
- Для типов
record
иreadonly record struct
: свойство только для инициализации. - Для типов
record struct
: свойство для чтения и записи.
- Для типов
- Основной конструктор, параметры которого соответствуют позиционным параметрам в объявлении записи.
- Для типов структуры записей конструктор без параметров присваивает каждому полю значение по умолчанию.
- Метод
Deconstruct
с параметромout
создается для каждого позиционного параметра, предоставленного в объявлении записи. Этот метод деконструирует свойства, определенные с помощью позиционного синтаксиса, и игнорирует любые свойства, определенные с помощью стандартного синтаксиса.
Вы можете добавить атрибуты в любой из этих элементов, создаваемых компилятором из определения записи. Вы можете добавить целевой объект к любому атрибуту, который применяется к свойствам позиционной записи. В следующем примере System.Text.Json.Serialization.JsonPropertyNameAttribute применяется к каждому свойству записи Person
. Целевой объект property:
указывает, что атрибут применяется к свойству, созданному компилятором. Другие значения — field:
для применения атрибута к полю и param:
для применения атрибута к параметру.
/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")] string FirstName,
[property: JsonPropertyName("lastName")] string LastName);
В примере выше также показано, как создавать для записи комментарии XML-документации. Вы можете указать тег <param>
, чтобы добавить документацию для параметров первичного конструктора.
Если созданное автоматически реализованное определение свойства не является нужным, можно определить собственное свойство того же имени. Например, возможно, потребуется изменить доступность либо изменяемость или предоставить реализацию для метода доступа get
либо set
. Если вы объявляете свойство в источнике, его необходимо инициализировать из позиционного параметра записи. Если свойство является автоматически реализованным свойством, необходимо инициализировать это свойство. При добавлении резервного поля в источник необходимо инициализировать резервное поле. Созданный деконструктор использует определение свойства. Например, в следующем примере объявляются свойства FirstName
и LastName
для позиционной записи public
, но параметр позиционирования Id
ограничен до internal
. Этот синтаксис можно использовать для типов записей и структур записей.
public record Person(string FirstName, string LastName, string Id)
{
internal string Id { get; init; } = Id;
}
public static void Main()
{
Person person = new("Nancy", "Davolio", "12345");
Console.WriteLine(person.FirstName); //output: Nancy
}
В типе записи не обязательно объявлять позиционные свойства. Вы можете объявить запись без позиционных свойств или даже объявить другие поля и свойства, как показано в следующем примере:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; } = [];
};
Если вы определите свойства с использованием стандартного синтаксиса свойств, но опустите модификатор доступа, эти свойства неявно становятся private
.
Неизменяемость
Позиционная запись и структура позиционной записи только для чтения объявляют свойства только для инициализации. Структура позиционированной записи объявляет свойства, доступные для чтения и записи. Можно переопределить любое из этих значений по умолчанию, как показано в предыдущем разделе.
Неизменяемость может быть полезной, если требуется обеспечить потокобезопасность для типа, ориентированного на данные, или существует необходимость сохранять хэш-код в неизменном виде в хэш-таблице. Но все же неизменяемость пригодна не для всех сценариев работы с данными. Entity Framework Core, например, не поддерживает обновление с неизменяемыми типами сущностей.
Свойства только для инициализации, созданные на основе позиционных параметров (record class
и readonly record struct
) или путем указания методов доступа init
, имеют неполную неизменяемость. После инициализации вы не сможете изменить значения свойств с типом значения или ссылки на свойства ссылочного типа. Но вы можете изменить сами данные, на которые ссылается свойство ссылочного типа. В следующем примере показано, что содержимое неизменяемого свойства ссылочного типа (в нашем примере это массив) является по сути изменяемым:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234
person.PhoneNumbers[0] = "555-6789";
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}
Возможности, уникальные для типов записей, реализуются синтезированными компилятором методами, ни один из которых не нарушает неизменяемость путем изменения состояния объекта. Если не указано иное, синтезированные методы создаются для объявлений record
, record struct
и readonly record struct
.
Равенство значений
Если методы равенства не переопределяются или не заменяются, то тип, который вы объявляете, управляет определением равенства.
- Для типов
class
два объекта равны, если они ссылаются на один и тот же объект в памяти. - Для типов
struct
два объекта равны, если хранят одинаковые значения и имеют один и тот же тип. - Для типов с модификатором
record
(record class
,record struct
иreadonly record struct
), два объекта равны, если они имеют одинаковый тип и хранят одинаковые значения.
Определение равенства для record struct
совпадает с struct
. Разница заключается в том, что для struct
реализация находится в ValueType.Equals(Object) и зависит от отражения. Для записей реализация синтезируется компилятором и использует объявленные члены данных.
Для некоторых моделей данных требуется ссылочное равенство. Например, Entity Framework Core использует ссылочное равенство, чтобы гарантировать использование только одного экземпляра типа сущности в том случае, когда разные объекты концептуально являются одной сущностью. По этой причине записи и структуры записей не подходят для использования в качестве типов сущностей в Entity Framework Core.
Следующий пример демонстрирует равенство значений для типов записей:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
var phoneNumbers = new string[2];
Person person1 = new("Nancy", "Davolio", phoneNumbers);
Person person2 = new("Nancy", "Davolio", phoneNumbers);
Console.WriteLine(person1 == person2); // output: True
person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True
Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}
Для реализации равенства значений компилятор синтезирует несколько методов, в том числе:
Переопределение Object.Equals(Object). Это ошибка, если переопределение объявляется явно.
Этот метод используется как основа для статического метода Object.Equals(Object, Object), если оба параметра имеют отличное от NULL значение.
sealed
Илиvirtual
,Equals(R? other)
гдеR
находится тип записи. Этот метод реализует IEquatable<T>. Этот метод можно объявить явным образом.Если тип записи является производным от базового типа
Base
записи, .Equals(Base? other)
Это ошибка, если переопределение объявляется явно. Если вы предоставляете собственную реализациюEquals(R? other)
, укажите также реализациюGetHashCode
.Переопределение Object.GetHashCode(). Этот метод можно объявить явным образом.
Переопределения операторов
==
и!=
. Это ошибка, если операторы объявлены явным образом.Если тип записи является производным от базового типа записи, .
protected override Type EqualityContract { get; };
Это свойство можно объявить явным образом. Дополнительные сведения см. в разделе "Равенство в иерархиях наследования".
Компилятор не синтезировал метод, если тип записи имеет метод, соответствующий сигнатуре синтезированного метода, который может быть объявлен явным образом.
Обратимое изменение
Если нужно копировать экземпляр с изменениями, вы можете с помощью выражения with
выполнить обратимое изменение. Выражение with
создает новый экземпляр записи, который является копией существующего экземпляра записи, и изменяет в этой копии указанные свойства и поля. Для указания требуемых изменений используется синтаксис инициализатора объектов, как показано в следующем примере:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; }
}
public static void Main()
{
Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
Console.WriteLine(person1);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Person person2 = person1 with { FirstName = "John" };
Console.WriteLine(person2);
// output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { PhoneNumbers = new string[1] };
Console.WriteLine(person2);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { };
Console.WriteLine(person1 == person2); // output: True
}
Выражение with
может задавать позиционные свойства или свойства, созданные с помощью стандартного синтаксиса свойств. Явно объявленные свойства должны init
иметь или set
метод доступа для изменения в with
выражении.
Результатом выражения with
является неполная копия, то есть для ссылочных свойств в нее копируется только ссылка на экземпляр. В итоге исходная запись и ее копия указывают на один и тот же экземпляр.
Для реализации такой возможности для типов record class
компилятор синтезирует метод клонирования и конструктор копии. Метод виртуального клонирования возвращает новую запись, инициализированную конструктором копий. При использовании выражения with
компилятор создает код, который вызывает метод клона, а затем устанавливает указанные в выражении with
свойства.
Если требуется другое поведение копирования, вы можете написать собственный конструктор копирования в record class
. При этом компилятор не синтезируется. Присвойте конструктору атрибут private
, если запись является sealed
, или protected
в противном случае. Компилятор не выполняет синтезирование конструктора копии для типов record struct
. Вы можете написать его, но компилятор не создает вызовы для with
выражений. При назначении объекта копируются значения record struct
.
Вы не можете переопределить метод клонирования или создать другой элемент с именем Clone
в любом типе записи. Фактическое имя метода клонирования создается компилятором.
Встроенное форматирование для отображения
Типы записей имеют создаваемый компилятором метод ToString, который отображает имена и значения открытых свойств и полей. Метод ToString
возвращает строку в следующем формате:
<имя> типа записи { <имя свойства> = <значение>, <имя> свойства = <значение>, ...}
Строка, которая выводится для <value>
, — это строка, возвращаемая ToString() для типа свойства. В следующем примере ChildNames
используется , System.Arrayгде ToString
возвращается System.String[]
:
Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }
Для реализации этой функциональной возможности в типах record class
компилятор синтезирует виртуальный метод PrintMembers
и переопределение ToString. В типах record struct
этот член является private
.
Переопределение ToString
создает объект StringBuilder с именем типа, за которым следует открывающая квадратная скобка. Затем оно вызывает метод PrintMembers
, который добавляет имена и значения свойств, а затем добавляет закрывающую скобку. В следующем примере показан код, аналогичный синтезированному переопределению:
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Teacher"); // type name
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}
Вы можете предоставить собственную реализацию PrintMembers
или переопределения ToString
. Примеры можно изучить в разделе Форматирование PrintMembers
в производных записях далее в этой статье. В C# 10 и более поздних версиях ваша реализация ToString
может включать модификатор sealed
, который не позволяет компилятору создавать реализацию ToString
для любых производных записей. Вы можете создать согласованное строковое представление в иерархии record
типов. (Производные записи по-прежнему PrintMembers
имеют метод, созданный для всех производных свойств.)
Наследование
Этот раздел относится только к типам record class
.
Запись может наследовать от другой записи. Но запись не может наследовать от класса, а класс не может наследовать от записи.
Позиционные параметры в производных типах записей
Производная запись объявляет позиционные параметры для всех параметров, определенных в основном конструкторе базовой записи. Базовая запись объявляет и инициализирует эти свойства. Производная запись не скрывает их, но создает и инициализирует только свойства для параметров, которые не объявлены в базовой записи.
Следующий пример демонстрирует наследование с использованием синтаксиса позиционных свойств:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
Равенство в иерархиях наследования
Этот раздел относится к типам record class
, но не к типам record struct
. Чтобы две переменные записи считались равными, у них должен совпадать тип времени выполнения. При этом типы содержащихся в них переменных могут отличаться. В следующем примере кода показано сравнение на унаследованное равенство:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Person student = new Student("Nancy", "Davolio", 3);
Console.WriteLine(teacher == student); // output: False
Student student2 = new Student("Nancy", "Davolio", 3);
Console.WriteLine(student2 == student); // output: True
}
В этом примере все переменные объявляются как Person
, даже если экземпляр является производным типом Student
или Teacher
. Все экземпляры имеют одинаковые свойства и одинаковые значения этих свойств. Но student == teacher
возвращает False
, хотя обе переменные имеют тип Person
, а student == student2
возвращает True
, хотя одна переменная имеет тип Person
, а другая — Student
. Проверка на равенство зависит от типа фактического объекта в среде выполнения, а не от объявленного типа переменной.
Чтобы реализовать такое поведение, компилятор синтезирует свойство EqualityContract
, которое возвращает объект Type, соответствующий типу записи. EqualityContract
позволяет методам равенства учитывать тип времени выполнения при проверке объектов на равенство. Если запись имеет базовый тип object
, это свойство получает атрибут virtual
. Если базовый тип имеет другой тип записи, это свойство становится переопределением. Если тип записи — sealed
, это свойство фактически имеет значение sealed
, так как тип имеет значение sealed
.
Если код сравнивает два экземпляра производного типа, синтезированные методы равенства проверяют все элементы данных базовых и производных типов для равенства. Синтезированный GetHashCode
метод использует GetHashCode
метод из всех элементов данных, объявленных в базовом типе и производном типе записи. Члены данных включают все объявленные поля и поле резервной record
копии компилятора для любых автоматически реализованных свойств.
Выражения with
в производных записях
Результат выражения with
имеет тот же тип среды выполнения, что и операнд выражения. Копируются все свойства с типом времени выполнения, но изменять вы можете только свойства с типом времени компиляции, как показано в следующем примере:
public record Point(int X, int Y)
{
public int Zbase { get; set; }
};
public record NamedPoint(string Name, int X, int Y) : Point(X, Y)
{
public int Zderived { get; set; }
};
public static void Main()
{
Point p1 = new NamedPoint("A", 1, 2) { Zbase = 3, Zderived = 4 };
Point p2 = p1 with { X = 5, Y = 6, Zbase = 7 }; // Can't set Name or Zderived
Console.WriteLine(p2 is NamedPoint); // output: True
Console.WriteLine(p2);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = A, Zderived = 4 }
Point p3 = (NamedPoint)p1 with { Name = "B", X = 5, Y = 6, Zbase = 7, Zderived = 8 };
Console.WriteLine(p3);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = B, Zderived = 8 }
}
Форматирование PrintMembers
в производных записях
Синтезированный метод PrintMembers
из производного типа записи вызывает базовую реализацию. Это означает, что в выходные данные ToString
включаются все свойства и поля с атрибутом public, как в производных, так и базовых типах, как показано в следующем примере:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
Вы можете предоставить собственную реализацию метода PrintMembers
. В этом случае используйте следующую сигнатуру:
- Для записи
sealed
, которая является производной отobject
(не объявляет базовую запись):private bool PrintMembers(StringBuilder builder)
. - для записи
sealed
, производной от другой записи (обратите внимание, что включающий тип —sealed
, поэтому метод фактически имеет значениеsealed
):protected override bool PrintMembers(StringBuilder builder)
; - Для записи, которая не является
sealed
и наследует от объекта:protected virtual bool PrintMembers(StringBuilder builder);
. - Для записи, которая не является
sealed
и наследует от другой записи:protected override bool PrintMembers(StringBuilder builder);
.
Ниже приведен пример кода, который заменяет синтезированные методы PrintMembers
: один пример для типа записи, которая наследует от объекта, и другой для типа записи, которая наследует от другой записи:
public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
return true;
}
}
public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
: Person(FirstName, LastName, PhoneNumbers)
{
protected override bool PrintMembers(StringBuilder stringBuilder)
{
if (base.PrintMembers(stringBuilder))
{
stringBuilder.Append(", ");
};
stringBuilder.Append($"Grade = {Grade}");
return true;
}
};
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", new string[2] { "555-1234", "555-6789" }, 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, PhoneNumber1 = 555-1234, PhoneNumber2 = 555-6789, Grade = 3 }
}
Примечание.
В C# 10 и более поздних версий компилятор будет синтезировать PrintMembers
в производных записях, даже если базовая запись запечатала метод ToString
. Вы также можете создать собственную реализацию PrintMembers
.
Поведение деконструктора в производных записях
Метод Deconstruct
производной записи возвращает значения всех позиционных свойств с типом времени компиляции. Если переменная имеет тип базовой записи, деконструкция выполняется только для свойств базовой записи, если объект не приведен к производному типу. Следующий пример демонстрирует вызов деконструктора для производной записи.
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
var (firstName, lastName) = teacher; // Doesn't deconstruct Grade
Console.WriteLine($"{firstName}, {lastName}");// output: Nancy, Davolio
var (fName, lName, grade) = (Teacher)teacher;
Console.WriteLine($"{fName}, {lName}, {grade}");// output: Nancy, Davolio, 3
}
Общие ограничения
Ключевое record
слово является модификатором для типа или struct
типаclass
. record
Добавление модификатора включает в себя поведение, описанное ранее в этой статье. Не существует общих ограничений, по которым тип обязан являться записью. Значение record class
удовлетворяет ограничению class
. Значение record struct
удовлетворяет ограничению struct
. Дополнительные сведения см. в статье Ограничения параметров типа.
Спецификация языка C#
Дополнительные сведения см. в разделе о классах в спецификации языка C#.
Дополнительные сведения об этих функциях см. в следующих заметках о предложении функций: