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


Структуры записей

Заметка

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

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия зафиксированы в соответствующих собраниях по проектированию языка (LDM).

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

Проблема чемпиона: https://github.com/dotnet/csharplang/issues/4334

Синтаксис структуры записи выглядит следующим образом:

record_struct_declaration
    : attributes? struct_modifier* 'partial'? 'record' 'struct' identifier type_parameter_list?
      parameter_list? struct_interfaces? type_parameter_constraints_clause* record_struct_body
    ;

record_struct_body
    : struct_body
    | ';'
    ;

Типы структур записей — это типы значений, такие как другие типы структур. Они неявно наследуются от класса System.ValueType. Модификаторы и члены структуры записей подвергаются тем же ограничениям, что и структуры (специальные возможности для типов, модификаторов элементов, base(...) инициализаторов экземпляров, определенное назначение для this в конструкторе, деструкторах, ...). Структуры записей также будут следовать тем же правилам, что и структуры для конструкторов экземпляров без параметров и инициализаторов полей, но в этом документе предполагается, что эти ограничения будут отменены для структур, как правило.

См. раздел §16.4.9 и спецификацию конструкторов структуры без параметров.

Структуры записей не могут использовать модификатор ref.

По крайней мере одно частичное объявление типа структуры частичной записи может предоставить parameter_list. parameter_list может быть пустым.

Параметры структуры записей не могут использовать модификаторы ref, out или this (но разрешены in и params).

Члены структуры записи

Помимо элементов, объявленных в тексте структуры записи, тип структуры записи содержит дополнительные синтезированные элементы. Члены синтезируются, если член с совпадающей подписью не объявлен в теле структуры записи или если унаследован доступный конкретный не виртуальный член с совпадающей подписью. Два члена считаются совпадающими, если у них одинаковая подпись или они рассматриваются как "сокрытие" в сценарии наследования. См. раздел "Подписи и перегрузка" §7.6. Ошибка, если у члена структуры записи имя "Клон".

Наличие небезопасного типа для поля экземпляра структуры записи является ошибкой.

Структурам записи не разрешается объявлять деструкторы.

Синтезированные члены приведены следующим образом:

Участники равноправия

Синтезированные члены равенства похожи на класс записей (Equals для этого типа, Equals для типа object, == и != операторы для этого типа).
за исключением отсутствия EqualityContract, проверок null или наследования.

Структуру записей реализует System.IEquatable<R> и включает синтезированную строго типизированную перегрузку Equals(R other), где R является структурой записей. Метод public. Метод можно объявить явным образом. Это ошибка, если явное объявление не соответствует ожидаемой сигнатуре или уровню доступности.

Если Equals(R other) определяется пользователем (не синтезируется), но GetHashCode нет, создается предупреждение.

public readonly bool Equals(R other);

Синтезированный Equals(R) возвращает true тогда и только тогда, когда для каждого поля экземпляра fieldN в структуре записи значение System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN), где TN — это тип поля, равно true.

В структуру записи входят синтезированные == и != операторы, эквивалентные операторам, объявленным следующим образом:

public static bool operator==(R r1, R r2)
    => r1.Equals(r2);
public static bool operator!=(R r1, R r2)
    => !(r1 == r2);

Метод Equals, вызываемый оператором ==, является методом Equals(R other), указанным выше. Оператор != делегирует оператору ==. Это ошибка, если операторы объявляются явно.

Структура записи содержит синтезированное переопределение, эквивалентное методу, объявленному следующим образом:

public override readonly bool Equals(object? obj);

Это ошибка, если переопределение объявляется явным образом. Синтезированное переопределение возвращает other is R temp && Equals(temp), где R является структурой записи.

Структура записи включает синтезированное переопределение, эквивалентное методу, объявленному следующим образом:

public override readonly int GetHashCode();

Метод можно объявить явным образом.

Предупреждение сообщается, если один из Equals(R) и GetHashCode() явно объявлен, но другой метод не является явным.

Синтезированное переопределение GetHashCode() возвращает int результат объединения значений System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) для каждого поля экземпляра fieldN, где TN является типом fieldN.

Например, рассмотрим следующую структуру записей:

record struct R1(T1 P1, T2 P2);

Для этой структуры записи синтезированные члены равенства будут выглядеть примерно так:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }
    public override bool Equals(object? obj) => obj is R1 temp && Equals(temp);
    public bool Equals(R1 other)
    {
        return
            EqualityComparer<T1>.Default.Equals(P1, other.P1) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R1 r1, R1 r2)
        => r1.Equals(r2);
    public static bool operator!=(R1 r1, R1 r2)
        => !(r1 == r2);    
    public override int GetHashCode()
    {
        return Combine(
            EqualityComparer<T1>.Default.GetHashCode(P1),
            EqualityComparer<T2>.Default.GetHashCode(P2));
    }
}

Элементы печати: методы PrintMembers и ToString

Структуру записи включает синтезированный метод, эквивалентный методу, объявленному следующим образом:

private bool PrintMembers(System.Text.StringBuilder builder);

Метод выполняет следующее:

  1. для каждого из печатаемых членов структуры записи (нестатическое публичное поле и члены доступных для чтения свойств), добавляет имя этого члена, за которым следует " = " и значение члена, разделенное ", ".
  2. возвращает true, если у структуры записи есть отображаемые элементы.

Для элемента, имеющего тип значения, мы преобразуем его значение в строковое представление с помощью наиболее эффективного метода, доступного для целевой платформы. В настоящее время это означает вызов ToString перед передачей в StringBuilder.Append.

Если в элементах записей, доступных для печати, отсутствует свойство, читаемое с методом доступа, отличным отreadonlyget, то синтезированный PrintMembers — это readonly. Нет требования, чтобы поля записи были readonly для метода PrintMembers, чтобы быть readonly.

Метод PrintMembers можно объявить явным образом. Это ошибка, если явное объявление не соответствует ожидаемой сигнатуре или доступности.

Структуру записи включает синтезированный метод, эквивалентный методу, объявленному следующим образом:

public override string ToString();

Если метод PrintMembers структуры данных readonly, то метод ToString(), синтезированный, readonly.

Метод можно объявить явным образом. Это ошибка, если явное объявление не соответствует ожидаемой сигнатуре или уровню доступа.

Синтезированный метод:

  1. создает экземпляр StringBuilder,
  2. добавляет имя структуры записи к построителю, за которым следует " { ",
  3. вызывает метод PrintMembers структуры записи, предоставляющий ему построитель, а затем — ", если он вернул значение true,
  4. добавляет "}"
  5. возвращает содержимое конструктора с builder.ToString().

Например, рассмотрим следующую структуру записей:

record struct R1(T1 P1, T2 P2);

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

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }

    private bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if P1 has a value type
        builder.Append(", ");

        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2.ToString()); if P2 has a value type

        return true;
    }

    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R1));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

Элементы структуры позиционной записи

Помимо указанных выше элементов, структуры записей со списком параметров ("позиционные записи") синтезируют дополнительные элементы с теми же условиями, что и члены выше.

Основной конструктор

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

record struct R1
{
    public R1() { } // ok
}

record struct R2()
{
    public R2() { } // error: 'R2' already defines constructor with same parameter types
}

Объявления полей экземпляра для структуры записей могут включать инициализаторы переменных. Если основной конструктор отсутствует, инициализаторы экземпляров выполняются как часть конструктора без параметров. В противном случае во время выполнения основной конструктор выполняет инициализаторы экземпляров, отображаемые в тексте записи-структуры.

Если у структуры записи есть основной конструктор, любой определяемый пользователем конструктор должен иметь явный this инициализатор конструктора, который вызывает первичный конструктор или явно объявленный конструктор.

Параметры основного конструктора, а также члены структуры записи являются доступными в инициализаторах полей или свойств экземпляра. Использование членов экземпляра будет ошибкой в этих местах (аналогично тому, как члены экземпляра доступны в области видимости обычных инициализаторов конструкторов, но их использование вызывает ошибку), но параметры основного конструктора будут доступны и использоваться, и будут затенять члены. Статические члены также могут использоваться.

Предупреждение создается, если параметр первичного конструктора не считывается.

Определенные правила назначения для конструкторов экземпляров структуры применяются к основному конструктору структур записей. Например, следующая ошибка:

record struct Pos(int X) // definite assignment error in primary constructor
{
    private int x;
    public int X { get { return x; } set { x = value; } } = X;
}

Свойства

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

Для структуры записи:

  • Создается общедоступное авто-свойство get и init, если структура записи имеет модификатор readonly; в противном случае, создаются get и set. Оба типа сет аксессоров (set и init) считаются идентичными. Пользователь может объявить свойство, которое устанавливается только при инициализации, вместо автоматически сгенерированного изменяемого свойства. Наследуемое свойство abstract с соответствующим типом переопределяется. Автоматическое свойство не создается, если у структуры записи есть поле экземпляра с ожидаемым именем и типом. Это ошибка, если унаследованное свойство не имеет publicget и set/init методы доступа. Это ошибка, если унаследованное свойство или поле скрыто.
    Автоматическое свойство инициализируется значением соответствующего основного параметра конструктора. Атрибуты можно применять к синтезируемому автоматическому свойству и его резервному полю, используя цели property: или field: для атрибутов, синтаксически применяемых к соответствующему параметру структуры записи.

Деконструкция

Структура позиционной записи с как минимум одним параметром синтезирует экземплярный метод с открытым доступом, возвращающий void, с именем Deconstruct и объявлением выходного параметра для каждого параметра основного конструктора. Каждый параметр метода Deconstruct имеет тот же тип, что и соответствующий параметр объявления первичного конструктора. Текст метода назначает каждому параметру метода Деконструкция значению из доступа члена экземпляра к элементу того же имени. Если члены экземпляра, доступ к которым осуществляется в теле, не включают свойство с методом доступаreadonlyget, то синтезированный метод Deconstruct является readonly. Метод можно объявить явным образом. Это ошибка, если явное объявление не соответствует ожидаемой сигнатуре или доступности, либо если оно статическое.

Разрешить выражение with для структур

Теперь допускается, чтобы получатель в выражении with имел тип структуры.

Справа от выражения with находится member_initializer_list, содержащая последовательность присваиваний идентификатору , который должен быть доступным полем экземпляра или свойством типа получателя.

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

Улучшения записей

Разрешить record class

Существующий синтаксис для типов записей позволяет record class в том же значении, что и record:

record_declaration
    : attributes? class_modifier* 'partial'? 'record' 'class'? identifier type_parameter_list?
      parameter_list? record_base? type_parameter_constraints_clause* record_body
    ;

Разрешить определяемым пользователем позиционным элементам быть полями

См. https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-10-05.md#changing-the-member-type-of-a-primary-constructor-parameter

Автоматическое свойство не создается, если в записи имеется или наследуется поле экземпляра с ожидаемым именем и типом.

Разрешить конструкторы без параметров и инициализаторы членов в структурах

См. спецификацию конструкторов структуры без параметров.

Открытые вопросы

  • как распознать структуры записей в метаданных? (У нас нет невыразимого метода клонирования для использования...)

Отвечено

  • убедитесь, что мы хотим сохранить дизайн PrintMembers (отдельный метод, возвращающий bool) (ответ: да)
  • подтвердите, что мы не разрешим record ref struct (проблема с полями IEquatable<RefStruct> и полями ссылок) (ответ: да)
  • подтвердите реализацию членов равенства. Альтернатива заключается в том, что синтезированные bool Equals(R other), bool Equals(object? other) и операторы все просто делегируют к ValueType.Equals. (ответ: да)
  • подтвердите, что мы хотим разрешить инициализаторы полей при наличии главного конструктора. Хотим ли мы также разрешить конструкторы структур без параметров, раз уж мы этим заняты (проблема с "Activator," по-видимому, устранена)? (ответ: да, обновленная спецификация должна быть проверена в LDM)
  • сколько мы хотим сказать о методе Combine? (ответ: как можно меньше)
  • следует ли запретить определяемый пользователем конструктор с сигнатурой конструктора копирования? (ответ: нет, нет понятия конструктора копирования в спецификации структуры записей)
  • Подтвердите, что мы хотим запретить членам с именем «Клон». (ответ: правильно)
  • дважды проверьте, что синтезированная Equals логика функционально эквивалентна реализации среды выполнения (например, float. NaN) (ответ: подтверждено в LDM)
  • могут ли атрибуты полей или свойств быть помещены в список позиционных параметров? (ответ: да, то же самое, что и для класса записи)
  • with обобщенных типов? (ответ: не входит в круг задач для C# 10)
  • должен ли GetHashCode включать хэш самого типа, чтобы получить разные значения между record struct S1; и record struct S2;? (ответ: нет)