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


Записи

Заметка

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

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

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

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

Это предложение отслеживает спецификацию функции записей C# 9, как было согласовано командой разработки языка C#.

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

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

record_base
    : ':' class_type argument_list?
    | ':' interface_type_list
    | ':' class_type argument_list? ',' interface_type_list
    ;

record_body
    : '{' class_member_declaration* '}' ';'?
    | ';'
    ;

Типы записей — это ссылочные типы, аналогичные объявлению класса. Ошибкой является предоставление записи record_baseargument_list, если record_declaration не содержит parameter_list. По крайней мере одно частичное объявление типа частичной записи может предоставить parameter_list.

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

Наследство

Записи не могут наследоваться от классов, если класс не object, а классы не могут наследоваться от записей. Записи могут наследоваться от других записей.

Элементы типа записи

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

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

Члены равенства

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

Type EqualityContract { get; }

Свойство private, если тип записи sealed. В противном случае, свойство будет virtual и protected. Свойство можно объявить явным образом. Это ошибка, если явное объявление не соответствует ожидаемой сигнатуре или доступности, или если явное объявление не позволяет переопределить его в производном типе, а тип записи не является sealed.

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

protected override Type EqualityContract { get; }

Свойство можно объявить явным образом. Это ошибка, если явное объявление не соответствует ожидаемой сигнатуре или доступности, или если явное объявление не позволяет переопределить его в производном типе, а тип записи не является sealed. Это ошибка, если синтезированное или явно объявленное свойство не переопределяет свойство с данной сигнатурой в типе записи Base (например, если свойство отсутствует в Base, или зафиксировано, или не виртуальное и так далее). Синтезируемое свойство возвращает typeof(R), где R является типом записи.

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

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

public virtual bool Equals(R? other);

Синтезированный Equals(R?) возвращает true тогда и только тогда, когда каждое из следующих условий true:

  • other не null, и
  • Для каждого поля экземпляра fieldN в типе записи, который не наследуется, значение System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN), где TN является типом поля, и
  • Если имеется базовый тип записи, значение base.Equals(other) (невиртуальный вызов к public virtual bool Equals(Base? other)), в противном случае значение EqualityContract == other.EqualityContract.

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

public static bool operator==(R? left, R? right)
    => (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R? left, R? right)
    => !(left == right);

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

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

public sealed override bool Equals(Base? other);

Это ошибка, если переопределение объявляется явным образом. Это ошибка, если метод не переопределяет метод с той же сигнатурой в типе записи Base (например, если метод отсутствует в Base, или запечатан, или не виртуальный, и т. д.). Синтезированное переопределение возвращает Equals((object?)other).

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

public override bool Equals(object? obj);

Это ошибка, если переопределение объявляется явным образом. Это ошибка, если метод не переопределяет object.Equals(object? obj) (например, из-за тени в промежуточных базовых типах и т. д.). Синтезированное переопределение возвращает Equals(other as R), где R — это тип записи.

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

public override int GetHashCode();

Метод можно объявить явным образом. Это ошибка, если явное объявление не позволяет его переопределить в производном типе, а тип записи не является sealed. Это ошибка, если синтезированный или явно объявленный метод не переопределяет object.GetHashCode() (например, из-за тени в промежуточных базовых типах и т. д.).

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

Синтезированное переопределение GetHashCode() возвращает результат int от объединения следующих значений:

  • Для каждого поля экземпляра fieldN в типе записи, который не наследуется, значение System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN), где TN является типом поля, и
  • Если имеется базовый тип записи, значение base.GetHashCode(); в противном случае значение System.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract).

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

record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);

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

class R1 : IEquatable<R1>
{
    public T1 P1 { get; init; }
    protected virtual Type EqualityContract => typeof(R1);
    public override bool Equals(object? obj) => Equals(obj as R1);
    public virtual bool Equals(R1? other)
    {
        return !(other is null) &&
            EqualityContract == other.EqualityContract &&
            EqualityComparer<T1>.Default.Equals(P1, other.P1);
    }
    public static bool operator==(R1? left, R1? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R1? left, R1? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),
            EqualityComparer<T1>.Default.GetHashCode(P1));
    }
}

class R2 : R1, IEquatable<R2>
{
    public T2 P2 { get; init; }
    protected override Type EqualityContract => typeof(R2);
    public override bool Equals(object? obj) => Equals(obj as R2);
    public sealed override bool Equals(R1? other) => Equals((object?)other);
    public virtual bool Equals(R2? other)
    {
        return base.Equals((R1?)other) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R2? left, R2? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R2? left, R2? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(),
            EqualityComparer<T2>.Default.GetHashCode(P2));
    }
}

class R3 : R2, IEquatable<R3>
{
    public T3 P3 { get; init; }
    protected override Type EqualityContract => typeof(R3);
    public override bool Equals(object? obj) => Equals(obj as R3);
    public sealed override bool Equals(R2? other) => Equals((object?)other);
    public virtual bool Equals(R3? other)
    {
        return base.Equals((R2?)other) &&
            EqualityComparer<T3>.Default.Equals(P3, other.P3);
    }
    public static bool operator==(R3? left, R3? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R3? left, R3? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(),
            EqualityComparer<T3>.Default.GetHashCode(P3));
    }
}

Копирование и клонирование элементов

Тип записи содержит два элемента копирования:

  • Конструктор, принимаюющий один аргумент типа записи. Это называется "конструктор копирования".
  • Синтезированный публичный метод-экземпляр "клонировать" без параметров с зарезервированным компилятором именем

Назначение конструктора копирования состоит в том, чтобы скопировать состояние из параметра в создаваемый новый экземпляр. Этот конструктор не запускает инициализаторы полей или свойств экземпляра, присутствующих в объявлении record. Если конструктор не объявлен явным образом, конструктор будет синтезирован компилятором. Если запись запечатана, конструктор будет закрытым, в противном случае он будет защищен. Явно объявленный конструктор копирования должен быть общедоступным или защищенным, если запись не запечатана. Первое, что необходимо сделать конструктору, — вызвать конструктор копирования базы или конструктор объекта без параметров, если запись наследуется от объекта. Сообщается об ошибке, если определяемый пользователем конструктор копирования использует неявный или явный инициализатор конструктора, который не соответствует этому требованию. После вызова конструктора базового копирования синтезированный конструктор копирования копирует значения всех полей экземпляра, неявно или явно объявленных в типе записи. Единственное присутствие конструктора копирования, явное или неявное, не предотвращает автоматическое добавление конструктора экземпляра по умолчанию.

Если в базовой записи присутствует виртуальный метод clone, то синтезированный метод clone его переопределяет, и возвращаемый тип метода — это тип текущего объекта. Ошибка возникает, если базовый метод клонирования записей запечатан. Если виртуальный метод clone отсутствует в базовой записи, возвращаемый тип метода клона является типом, содержащим тип, и метод является виртуальным, если запись не запечатана или абстрактна. Если содержащая запись абстрактна, синтезированный метод клона также абстрактен. Если метод clone не абстрактен, он возвращает результат вызова конструктора копирования.

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

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

bool PrintMembers(System.Text.StringBuilder builder);

Метод — private, если тип записи — sealed. В противном случае используется метод virtual и protected.

Метод:

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

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

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

protected override bool PrintMembers(StringBuilder builder);

Если запись не содержит печатных элементов, метод вызывает базовый метод PrintMembers с одним аргументом (его параметром builder) и возвращает результат.

В противном случае метод:

  1. вызывает базовый метод PrintMembers с одним аргументом (его параметром builder);
  2. Значение true, если метод PrintMembers вернул true, добавьте ", " в построителе,
  3. для каждого из печатных элементов записи добавляет имя элемента, за которым следует "= ", и значение элемента: this.member (или this.member.ToString() для типов значений), разделённых ", ".
  4. возвращает значение true.

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

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

public override string ToString();

Метод можно объявить явным образом. Это ошибка, если явное объявление не соответствует ожидаемой сигнатуре или доступности, или если явное объявление не позволяет переопределить его в производном типе, а тип записи не является sealed. Это ошибка, если синтезированный или явно объявленный метод не переопределяет object.ToString() (например, из-за тени в промежуточных базовых типах и т. д.).

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

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

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

record R1(T1 P1);
record R2(T1 P1, T2 P2, T3 P3) : R1(P1);

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

class R1 : IEquatable<R1>
{
    public T1 P1 { get; init; }
    
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if T1 is 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();
    }
}

class R2 : R1, IEquatable<R2>
{
    public T2 P2 { get; init; }
    public T3 P3 { get; init; }
    
    protected override bool PrintMembers(StringBuilder builder)
    {
        if (base.PrintMembers(builder))
            builder.Append(", ");
            
        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2); if T2 is a value type
        
        builder.Append(", ");
        
        builder.Append(nameof(P3));
        builder.Append(" = ");
        builder.Append(this.P3); // or builder.Append(this.P3); if T3 is a value type
        
        return true;
    }
    
    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R2));
        builder.Append(" { ");

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

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

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

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

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

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

Во время выполнения основной конструктор

  1. выполняет инициализаторы экземпляров, отображаемые в теле класса

  2. вызывает конструктор базового класса с аргументами, указанными в предложении record_base, если он присутствует

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

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

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

Переменные выражения, объявленные в argument_list, находятся в области видимости argument_list. Те же правила затенения применяются так же, как и в списке аргументов инициализатора конструктора.

Свойства

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

Для протокола:

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

Деконструируйте

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

В следующем примере показана позиционная запись R с методом Deconstruct, синтезированным компилятором, а также её использование:

public record R(int P1, string P2 = "xyz")
{
    public void Deconstruct(out int P1, out string P2)
    {
        P1 = this.P1;
        P2 = this.P2;
    }
}

class Program
{
    static void Main()
    {
        R r = new R(12);
        (int p1, string p2) = r;
        Console.WriteLine($"p1: {p1}, p2: {p2}");
    }
}

выражение with

Выражение with — это новое выражение с помощью следующего синтаксиса.

with_expression
    : switch_expression
    | switch_expression 'with' '{' member_initializer_list? '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : identifier '=' expression
    ;

Выражение with не допускается в качестве инструкции.

Выражение with позволяет выполнять "недеструктивное изменение", создавая копию выражения приемника с изменениями назначений в member_initializer_list.

Допустимое выражение with имеет приемник с ненулевым типом. Тип приемника должен быть записью.

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

Во-первых, вызывается метод "клонирования" приемника (указанного выше), и его результат преобразуется в тип получателя. Затем каждый member_initializer обрабатывается так же, как назначение для поля или доступа к свойству результата преобразования. Назначения обрабатываются в лексическом порядке.