Ler em inglês

Compartilhar via


Registros

Observação

Este artigo é uma especificação de recurso. A especificação age como o documento de design para o recurso. Ela inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas divergências entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de idioma (LDM).

Você pode saber mais sobre o processo de adoção de speclets de recursos no padrão de linguagem C# no artigo sobre as especificações de .

Problema do especialista: https://github.com/dotnet/csharplang/issues/39

Essa proposta acompanha a especificação do recurso de registros C# 9, conforme acordado pela equipe de design de linguagem C#.

A sintaxe de um registro é a seguinte:

antlr
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* '}' ';'?
    | ';'
    ;

Os tipos de registro são tipos de referência, semelhantes a uma declaração de classe. É um equívoco que um registro forneça um record_baseargument_list se o record_declaration não contiver um parameter_list. No máximo, uma declaração de tipo parcial de um registro parcial é capaz de fornecer um parameter_list.

Os parâmetros de registro não podem usar modificadores ref, out ou this (mas são permitidos in e params).

Herança

Os registros não podem herdar de classes, a menos que a classe seja object, e as classes não podem herdar de registros. Os registros podem herdar de outros registros.

Membros de um tipo de registro

Além dos membros declarados no corpo do registro, um tipo de registro também tem membros sintetizados adicionais. Os membros são sintetizados, exceto quando um membro com uma assinatura "correspondente" é declarado no corpo do registro ou quando é herdado um membro concreto não virtual acessível com uma assinatura "correspondente". Um membro correspondente impede que o compilador gere esse membro, e não outros membros sintetizados. Dois membros são considerados correspondentes se tiverem a mesma assinatura ou forem considerados "ocultos" em um cenário de herança. É um erro que um membro de um registro seja chamado de "Clone". Caso um campo de instância de um registro tenha um tipo de ponteiro de nível superior, trata-se de um erro. Um tipo de ponteiro aninhado, como uma matriz de ponteiros, é permitido.

Os membros sintetizados são os seguintes:

Membros de igualdade

Se o registro for derivado de object, o tipo de registro incluirá uma propriedade sintetizada de somente leitura que equivale a uma propriedade declarada da seguinte maneira:

C#
Type EqualityContract { get; }

A propriedade é private se o tipo de registro for sealed. Caso contrário, a propriedade é virtual e protected. A propriedade pode ser declarada explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada ou se a declaração explícita não permitir substituição em um tipo derivado e o tipo de registro não for sealed.

Se o tipo de registro for derivado de um tipo de registro base Base, o tipo de registro incluirá uma propriedade somente leitura sintetizada equivalente a uma propriedade declarada da seguinte maneira:

C#
protected override Type EqualityContract { get; }

A propriedade pode ser declarada explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada ou se a declaração explícita não permitir substituição em um tipo derivado e o tipo de registro não for sealed. Trata-se de um erro se a propriedade sintetizada ou declarada explicitamente não substituir uma propriedade com essa assinatura no tipo de registro Base (por exemplo, se a propriedade não for encontrada no Base, ou se for selada ou não virtual, etc.). A propriedade sintetizada retorna typeof(R), onde R é o tipo de registro.

O tipo de registro implementa System.IEquatable<R> e inclui uma sobrecarga sintetizada com tipagem forte de Equals(R? other), onde R é o tipo de registro. O método é public, e o método é virtual a menos que o tipo de registro seja sealed. O método pode ser declarado explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada ou se a declaração explícita não permitir substituição em um tipo derivado e o tipo de registro não for sealed.

Se Equals(R? other) for definido pelo usuário (não sintetizado), mas GetHashCode não for, um aviso será emitido.

C#
public virtual bool Equals(R? other);

O Equals(R?) sintetizado retorna true se e somente se cada um dos seguintes forem true:

  • other não é null, e
  • Para cada campo de instância fieldN no tipo de registro que não é herdado, o valor de System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) onde TN é o tipo de campo e
  • Se houver um tipo de registro básico, o valor será base.Equals(other) (uma chamada não virtual para public virtual bool Equals(Base? other)); caso contrário, o valor será EqualityContract == other.EqualityContract.

O tipo de registro inclui operadores == e != sintetizados equivalentes aos operadores declarados da seguinte maneira:

C#
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);

O método Equals chamado pelo operador == é o método Equals(R? other) especificado acima. O operador != delega ao operador ==. Será um erro se os operadores forem declarados explicitamente.

Se o tipo de registro for derivado de um tipo de registro base Base, o tipo de registro incluirá uma propriedade somente de substituição sintetizada equivalente a um método declarado da seguinte maneira:

C#
public sealed override bool Equals(Base? other);

Será um erro se a substituição for declarada explicitamente. Será um erro se o método não substituir um método com a mesma assinatura no tipo de registro Base (por exemplo, se o método não for encontrado no Base, ou for selado ou não virtual, etc.). A substituição sintetizada retorna Equals((object?)other).

O tipo de registro inclui uma substituição sintetizada equivalente a um método declarado da seguinte maneira:

C#
public override bool Equals(object? obj);

Será um erro se a substituição for declarada explicitamente. É um erro se o método não substituir object.Equals(object? obj) (por exemplo, devido a sombreamento em tipos base intermediários etc.). A substituição sintetizada retorna Equals(other as R) ONDE R é o tipo de registro.

O tipo de registro inclui uma substituição sintetizada equivalente a um método declarado da seguinte maneira:

C#
public override int GetHashCode();

O método pode ser declarado explicitamente. Será um erro se a declaração explícita não permitir substituição em um tipo derivado e o tipo de registro não for sealed. É um erro se o método sintetizado ou explicitamente declarado não substituir object.GetHashCode() (por exemplo, devido ao sombreamento em tipos de base intermediários, etc.).

Um aviso será emitido se um dos Equals(R?) e GetHashCode() for declarado explicitamente, mas o outro método não for declarado explicitamente.

A substituição sintetizada de GetHashCode() retorna um resultado int da combinação dos valores a seguir:

  • Para cada campo de instância fieldN no tipo de registro que não é herdado, o valor de System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) onde TN é o tipo de campo e
  • Se houver um tipo de registro base, o valor de base.GetHashCode(); caso contrário, o valor de System.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract).

Por exemplo, considere os seguintes tipos de registro:

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

Para esses tipos de registro, os membros de igualdade sintetizados seriam algo como:

C#
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));
    }
}

Copiar e clonar membros

Um tipo de registro contém dois membros de cópia:

  • Um construtor que usa um único argumento do tipo de registro. Ele é chamado de "construtor de cópia".
  • Um método de instância pública "clone" sem parâmetros, sintetizado com um nome reservado pelo compilador.

A finalidade do construtor de cópia é copiar o estado do parâmetro para a nova instância que está sendo criada. Esse construtor não executa nenhum inicializador de campo/propriedade de instância presente na declaração de registro. Se o construtor não for explicitamente declarado, um construtor será sintetizado pelo compilador. Se o registro for lacrado, o construtor será privado, caso contrário, ele será protegido. Um construtor de cópia declarado explicitamente deve ser público ou protegido, a menos que o registro seja lacrado. A primeira coisa que o construtor deve fazer é chamar um construtor de cópia da base ou um construtor de objeto sem parâmetro se o registro herdar do objeto. Um erro será relatado se um construtor de cópia definido pelo usuário usar um inicializador de construtor implícito ou explícito que não atenda a esse requisito. Depois que um construtor de cópia base é invocado, um construtor de cópia sintetizado copia valores para todos os campos de instância implicitamente ou explicitamente declarados dentro do tipo de registro. A presença de um construtor de cópia por si só, seja explícito ou implícito, não impede a adição automática de um construtor de instância padrão.

Se um método "clone" virtual estiver presente no registro base, o método "clone" sintetizado o substituirá e o tipo de retorno do método será o tipo atual que contém. Um erro será produzido se o método de clonagem do registro base estiver selado. Se um método de "clone" virtual não estiver presente no registro base, o tipo de retorno do método de clone será o tipo que o contém e o método será virtual, a menos que o registro seja selado ou abstrato. Se o registro de contenção for abstrato, o método de clone sintetizado também será abstrato. Se o método de "clone" não for abstrato, ele retornará o resultado de uma chamada para um construtor de cópia.

Impressão de membros: métodos PrintMembers e ToString

Se o registro for derivado de object, o registro incluirá um método sintetizado equivalente a um método declarado da seguinte maneira:

C#
bool PrintMembers(System.Text.StringBuilder builder);

O método é private se o tipo de registro for sealed. Caso contrário, o método será virtual e protected.

O método :

  1. chama o método System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack() se o método estiver presente e o registro tiver membros que possam ser impressos.
  2. para cada um dos membros imprimíveis do registro (campo público não estático e membros da propriedade legível), acrescenta o nome desse membro seguido por " = " seguido pelo valor do membro separado por ",
  3. retornará true se o registro tiver membros imprimíveis.

Para um membro que tem um tipo de valor, converteremos seu valor em uma representação de cadeia de caracteres usando o método mais eficiente disponível para a plataforma de destino. No momento, isso significa chamar ToString antes de passar para StringBuilder.Append.

Se o tipo de registro for derivado de um registro base Base, o registro incluirá uma propriedade somente de substituição sintetizada equivalente a um método declarado da seguinte maneira:

C#
protected override bool PrintMembers(StringBuilder builder);

Se o registro não tiver membros imprimíveis, o método chama o método base PrintMembers com um argumento (seu parâmetro builder) e retorna o resultado.

Caso contrário, o método:

  1. chama o método base PrintMembers com um argumento (seu parâmetro builder)
  2. Se o método PrintMembers retornar true, acrescente ", " ao builder.
  3. para cada um dos membros imprimíveis do registro, anexa o nome desse membro seguido por " = " seguido pelo valor do membro: this.member (ou this.member.ToString() para tipos de valor), separado com ",
  4. retorna true.

O método PrintMembers pode ser declarado explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada ou se a declaração explícita não permitir substituição em um tipo derivado e o tipo de registro não for sealed.

O registro inclui um método sintetizado equivalente a um método declarado da seguinte maneira:

C#
public override string ToString();

O método pode ser declarado explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada ou se a declaração explícita não permitir substituição em um tipo derivado e o tipo de registro não for sealed. É um erro se o método sintetizado ou explicitamente declarado não substituir object.ToString() (por exemplo, devido ao sombreamento em tipos de base intermediários, etc.).

O método sintetizado:

  1. cria uma instância StringBuilder,
  2. acrescenta o nome do registro ao construtor, seguido por " { ",
  3. invoca o método PrintMembers do registro, fornecendo-lhe o construtor e seguido por " " se ele retornar true,
  4. anexa "}",
  5. retorna o conteúdo do objeto builder com builder.ToString().

Por exemplo, considere os seguintes tipos de registro:

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

Para esses tipos de registro, os membros de impressão sintetizados seriam algo como:

C#
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();
    }
}

Membros de registro posicional

Além dos membros acima, os registros com uma lista de parâmetros ("registros posicionais") sintetizam membros adicionais com as mesmas condições que os membros acima.

Construtor primário

Um tipo de registro tem um construtor público cuja assinatura corresponde aos parâmetros de valor da declaração de tipo. Isso é chamado de construtor primário para o tipo e faz com que o construtor de classe padrão declarado implicitamente, se houver, seja suprimido. É um erro ter um construtor primário e um construtor com a mesma assinatura já presentes na classe.

Em runtime, o construtor primário

  1. executa os inicializadores de instância que aparecem no corpo da classe

  2. invoca o construtor de classe base com os argumentos fornecidos na cláusula record_base, se houver

Se um registro tiver um construtor primário, qualquer construtor definido pelo usuário, exceto "construtor de cópia", deverá ter um inicializador de construtor this explícito.

Os parâmetros do construtor primário, assim como os membros do registro, estão em escopo dentro do argument_list da cláusula record_base e nos inicializadores de campos de instância ou propriedades. Os membros da instância seriam um erro nesses locais (semelhante à forma como os membros da instância estão no escopo em inicializadores de construtores regulares atualmente, mas um erro de uso), mas os parâmetros do construtor primário estariam no escopo e seriam utilizáveis, além de serem membros sombreados. Também seria possível usar estáticos, semelhante à forma como as chamadas base e os inicializadores funcionam hoje em construtores comuns.

Um aviso será gerado se um parâmetro do construtor primário não for lido.

As variáveis de expressão declaradas no argument_list estão no escopo dentro do argument_list. Aplicam-se as mesmas regras de sombreamento que em uma lista de argumentos de um inicializador de construtor regular.

Propriedades

Para cada parâmetro individual de um tipo de registro, há um membro de propriedade pública correspondente cujo nome e tipo são derivados da declaração de parâmetro de valor.

Para registro:

  • Uma propriedade pública get e automática init é criada (consulte a init especificação do acessador separado). Uma propriedade herdada abstract com o tipo correspondente é substituída. É um erro se a propriedade herdada não tiver public substituíveis get e acessadores init. Se a propriedade herdada estiver oculta, trata-se de um erro.
    A propriedade automática é inicializada para o valor do parâmetro de construtor primário correspondente. Os atributos podem ser aplicados à propriedade automática sintetizada e ao seu campo de suporte usando alvos property: ou field: para atributos que são aplicados sintaticamente ao parâmetro de registro correspondente.

Desconstrução

Um registro posicional com pelo menos um parâmetro sintetiza um método de instância de retorno de void público chamado Deconstruct com uma declaração de parâmetro de saída para cada parâmetro da declaração do construtor primário. Cada parâmetro do método Deconstruct tem o mesmo tipo que o parâmetro correspondente da declaração do construtor primário. O corpo do método atribui o valor da propriedade da instância de mesmo nome a cada parâmetro do método Deconstruct. O método pode ser declarado explicitamente. Será um erro se a declaração explícita não corresponder à assinatura ou acessibilidade esperada ou for estática.

O exemplo a seguir mostra um registro posicional R com seu método Deconstruct sintetizado pelo compilador, juntamente com seu uso:

C#
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 expressão

Uma expressão with é uma nova expressão usando a sintaxe a seguir.

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

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : identifier '=' expression
    ;

Uma expressão with não é permitida como uma instrução.

Uma expressão with permite "mutação não destrutiva", projetada para produzir uma cópia da expressão receptora com modificações em atribuições no member_initializer_list.

Uma expressão with válida tem um receptor com um tipo não nulo. O tipo de receptor deve ser um registro.

No lado direito da expressão with está um member_initializer_list com uma sequência de atribuições para o identificador, o qual deve ser um campo de instância acessível ou propriedade do tipo do receptor.

Primeiro, o método de "clone" do receptor (especificado acima) é invocado e seu resultado é convertido no tipo do receptor. Em seguida, cada member_initializer é processado da mesma maneira que uma atribuição a um campo ou acesso à propriedade do resultado da conversão. As tarefas são processadas em ordem lexical.