Registros (referência em C#)

Use o modificador record para definir um tipo de referência que fornece funcionalidade interna para encapsular dados. O C# 10 permite que a sintaxe record class como um sinônimo esclareça um tipo de referência e record struct defina um tipo de valor com funcionalidade semelhante.

Quando você declara um construtor primário em um registro, o compilador gera propriedades públicas para os parâmetros do construtor primário. Os parâmetros do construtor primário para um registro são chamados de parâmetros posicionais. O compilador cria propriedades posicionais que espelham o construtor primário ou os parâmetros posicionais. O compilador não sintetiza propriedades para parâmetros de construtor primário em tipos que não têm o modificador record.

Os dois exemplos a seguir demonstram os tipos de referência record (ou record class):

public record Person(string FirstName, string LastName);
public record Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
};

Os dois exemplos a seguir demonstram tipos de valor 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; }
}

Você também pode criar registros com propriedades e campos mutáveis:

public record Person
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
};

Os structs de registro também podem ser mutáveis, tanto structs de registro posicional quanto structs de registro sem parâmetros posicionais:

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; }
}

Embora os registros possam ser mutáveis, eles se destinam principalmente a dar suporte a modelos de dados imutáveis. O tipo de registro oferece os seguintes recursos:

Os exemplos anteriores mostram algumas distinções entre registros que são tipos de referência e registros que são tipos de valor:

  • Um record ou um record class declara um tipo de referência. A palavra-chave class é opcional, mas pode adicionar clareza aos leitores. Um record struct declara um tipo de valor.
  • As propriedades posicionais são imutáveis em um record class e um readonly record struct. Eles são mutáveis em um record struct.

O restante deste artigo aborda os tipos record class e record struct. As diferenças são detalhadas em cada seção. Você deve decidir entre um record class e um record struct semelhante para decidir entre um class e um struct. O termo registro é usado para descrever o comportamento que se aplica a todos os tipos de registro. Tanto record struct quanto record class são usados para descrever o comportamento que se aplica a apenas struct ou tipos de classe, respectivamente. O tipo record struct foi introduzido no C# 10.

Sintaxe posicional para definição de propriedade

Você pode usar parâmetros posicionais para declarar propriedades de um registro e inicializar os valores de propriedade ao criar uma instância:

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 }
}

Quando você usa a sintaxe posicional para definição de propriedade, o compilador cria:

  • Uma propriedade pública implementada automaticamente para cada parâmetro posicional fornecido na declaração de registro.
    • Para tipos record e readonly record struct: uma propriedade init-only .
    • Para tipos record struct: uma propriedade read-write.
  • Um construtor primário cujos parâmetros correspondem aos parâmetros posicionais na declaração de registro.
  • Para tipos de struct de registro, um construtor sem parâmetros que define cada campo como seu valor padrão.
  • Um método Deconstruct com um parâmetro out para cada parâmetro posicional fornecido na declaração de registro. O método desconstrói propriedades definidas usando sintaxe posicional; ele ignora as propriedades definidas usando a sintaxe de propriedade padrão.

Talvez você queira adicionar atributos a qualquer um desses elementos que o compilador cria a partir da definição de registro. Você pode adicionar um destino a qualquer atributo aplicado às propriedades do registro posicional. O exemplo a seguir aplica-se a System.Text.Json.Serialization.JsonPropertyNameAttribute para cada propriedade do registro Person. O destino property: indica que o atributo é aplicado à propriedade gerada pelo compilador. Outros valores são field: para aplicar o atributo ao campo e param: para aplicar o atributo ao parâmetro.

/// <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);

O exemplo anterior também mostra como criar comentários de documentação XML para o registro. Você pode adicionar a marca <param> para adicionar documentação para os parâmetros do construtor primário.

Se a definição de propriedade gerada automaticamente não for o que você deseja, você poderá definir sua própria propriedade com o mesmo nome. Por exemplo, talvez você queira alterar a acessibilidade ou a mutabilidade ou fornecer uma implementação para o acessador get ou set. Se você declarar a propriedade em sua origem, deverá inicializá-la do parâmetro posicional do registro. Se sua propriedade for uma propriedade implementada automaticamente, você deverá inicializar a propriedade. Se você adicionar um campo de backup em sua origem, deverá inicializar o campo de suporte. O desconstrutor gerado usa sua definição de propriedade. Por exemplo, o exemplo a seguir declara as propriedades FirstName e LastName de um registro posicional public, mas restringe o parâmetro posicional Id a internal. Você pode usar essa sintaxe para registros e tipos de struct de registro.

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

}

Um tipo de registro não precisa declarar nenhuma propriedade posicional. Você pode declarar um registro sem nenhuma propriedade posicional e pode declarar outros campos e propriedades, como no exemplo a seguir:

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; } = [];
};

Se você definir propriedades usando a sintaxe de propriedade padrão, mas omitir o modificador de acesso, as propriedades serão implicitamente private.

Imutabilidade

Um registro posicional e um struct de registro de leitura posicional declaram propriedades init-only. Um struct de registro posicional declara propriedades read-write. Você pode substituir qualquer um desses padrões, conforme mostrado na seção anterior.

A imutabilidade pode ser útil quando você precisa de um tipo centrado em dados para ser seguro para threads ou se você depender de que um código hash permaneça o mesmo em uma tabela de hash. No entanto, a imutabilidade não é apropriada para todos os cenários de dados. O Entity Framework Core, por exemplo, não dá suporte à atualização com tipos de entidade imutáveis.

As propriedades init-only, independentemente de serem criadas a partir de parâmetros posicionais (record class e readonly record struct) ou especificando acessadores init, têm imutabilidade superficial. Após a inicialização, você não pode alterar o valor das propriedades do tipo valor ou a referência de propriedades de tipo de referência. No entanto, os dados aos quais uma propriedade de tipo de referência se refere podem ser alterados. O exemplo a seguir mostra que o conteúdo de uma propriedade imutável de tipo de referência (uma matriz nesse caso) é mutável:

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
}

Os recursos exclusivos para tipos de registro são implementados por métodos sintetizados pelo compilador e nenhum desses métodos compromete a imutabilidade por meio da modificação do estado do objeto. A menos que especificado, os métodos sintetizados são gerados para declarações record, record struct e readonly record struct.

Igualdade de valor

Se você não substituir nem sobrecarregar métodos de maneira igual, o tipo que você declarar definirá como a igualdade é definida:

  • Para tipos class, dois objetos serão iguais quando se referirem ao mesmo objeto na memória.
  • Para tipos struct, dois objetos são iguais se forem do mesmo tipo e armazenarem os mesmos valores.
  • Para tipos com o modificador record (record class, record struct e readonly record struct), dois objetos são iguais se forem do mesmo tipo e armazenarem os mesmos valores.

A definição de igualdade para um record struct é a mesma de um struct. A diferença é que, para um struct, a implementação está em ValueType.Equals(Object) e depende da reflexão. Para registros, a implementação é sintetizada pelo compilador e usa os membros de dados declarados.

A igualdade de referência é necessária para alguns modelos de dados. Por exemplo, o Entity Framework Core depende da igualdade de referência para garantir que ele use apenas uma instância de um tipo de entidade para o que é conceitualmente uma entidade. Por esse motivo, os registros e os structs de registro não são apropriados para uso como tipos de entidade no Entity Framework Core.

O exemplo a seguir ilustra a igualdade de valores dos tipos de registro:

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
}

Para implementar a igualdade de valor, o compilador sintetiza os vários métodos, incluindo:

  • Uma substituição de Object.Equals(Object). Será um erro se a substituição for declarada explicitamente.

    Esse método é usado como base para o método estático Object.Equals(Object, Object) quando ambos os parâmetros são não nulos.

  • Um virtual, ou sealed, Equals(R? other) em que R é o tipo de registro. Esse método implementa IEquatable<T>. Esse método pode ser declarado explicitamente.

  • Se o tipo de registro for derivado de um tipo de registro base Base, Equals(Base? other). Será um erro se a substituição for declarada explicitamente. Se você fornecer sua própria implementação de Equals(R? other), forneça também uma implementação de GetHashCode.

  • Uma substituição de Object.GetHashCode(). Esse método pode ser declarado explicitamente.

  • Substituições de operadores == e !=. Será um erro se os operadores forem declarados explicitamente.

  • Se o tipo de registro for derivado de um tipo de registro base protected override Type EqualityContract { get; };. Essa propriedade pode ser declarada explicitamente. Para obter mais informações, consulte Igualdade nas hierarquias de herança.

O compilador não sintetiza um método quando um tipo de registro tem um método que corresponde à assinatura de um método sintetizado que pode ser declarado explicitamente.

Mutação não destrutiva

Se você precisar copiar uma instância com algumas modificações, poderá usar uma expressão with para obter uma mutação não destrutiva. Uma expressão with faz uma nova instância de registro que é uma cópia de uma instância de registro existente, com propriedades e campos especificados modificados. Use a sintaxe do inicializador de objetos para especificar os valores a serem alterados, conforme mostrado no exemplo a seguir:

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
}

A expressão with pode definir propriedades posicionais ou propriedades criadas usando a sintaxe de propriedade padrão. As propriedades explicitamente declaradas devem ter um acessador init ou set para serem alteradas em uma expressão with.

O resultado de uma expressão with é uma cópia superficial, o que significa que, para uma propriedade de referência, somente a referência a uma instância é copiada. O registro original e a cópia acabam com uma referência à mesma instância.

Para implementar esse recurso para tipos record class, o compilador sintetiza um método clone e um construtor de cópia. O método clone virtual retorna um novo registro inicializado pelo construtor de cópia. Quando você usa uma expressão with, o compilador cria um código que chama o método clone e define as propriedades especificadas na expressão with.

Se você precisar de um comportamento de cópia diferente, poderá escrever seu próprio construtor de cópia em um record class. Se você fizer isso, o compilador não sintetizará. Faça o construtor private se o registro for sealed, caso contrário, faça protected. O compilador não sintetiza um construtor de cópia para tipos record struct. Você pode escrever, mas o compilador não gerará chamadas para expressões with. Os valores do record struct são copiados na atribuição.

Você não pode substituir o método clone e não pode criar um membro nomeado Clone em nenhum tipo de registro. O nome real do método clone é gerado pelo compilador.

Formatação interna para exibição

Os tipos de registro têm um método ToString gerado pelo compilador que exibe os nomes e valores de propriedades e campos públicos. O método ToString retorna uma cadeia de caracteres do seguinte formato:

<nome do tipo de registro> { <nome da propriedade> = <valor>, <nome da propriedade> = <valor>, ...}

A cadeia de caracteres impressa para <value> é a cadeia de caracteres retornada pelo ToString() para o tipo da propriedade. No exemplo a seguir, ChildNames é um System.Array, em que ToString retorna System.String[]:

Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }

Para implementar esse recurso em tipos record class, o compilador sintetiza um método PrintMembers virtual e uma substituição ToString. Em tipos record struct, esse membro é private. A substituição ToString cria um objeto StringBuilder com o nome do tipo seguido por um colchete de abertura. Ele chama PrintMembers para adicionar nomes e valores de propriedade e, em seguida, adiciona o colchete de fechamento. O exemplo a seguir mostra código semelhante ao que a substituição sintetizada contém:

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

Você pode fornecer sua própria implementação de PrintMembers ou substituição ToString. Exemplos são fornecidos na seção PrintMembersFormatação em registros derivados posteriormente neste artigo. No C# 10 e posterior, sua implementação ToString pode incluir o modificador, o sealed que impede que o compilador sintetize uma implementação ToString para quaisquer registros derivados. Você pode criar uma representação de cadeia de caracteres consistente em uma hierarquia de tipos record. (Os registros derivados ainda terão um método PrintMembers gerado para todas as propriedades derivadas.)

Herança

Esta seção se aplica somente a tipos record class.

Um registro pode herdar de outro registro. No entanto, um registro não pode herdar de uma classe, e uma classe não pode herdar de um registro.

Parâmetros posicionais em tipos de registro derivados

O registro derivado declara parâmetros posicionais para todos os parâmetros no construtor primário do registro base. O registro base declara e inicializa essas propriedades. O registro derivado não os oculta, mas apenas cria e inicializa propriedades para parâmetros que não são declarados em seu registro base.

O exemplo a seguir ilustra a herança com sintaxe de propriedade posicional:

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 }
}

Igualdade nas hierarquias de herança

Esta seção se aplica a tipos record class, mas não a tipos record struct. Para que duas variáveis de registro sejam iguais, o tipo de tempo de execução deve ser igual. Os tipos das variáveis de conteúdo podem ser diferentes. A comparação de igualdade herdada é ilustrada no exemplo de código a seguir:

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
}

No exemplo, todas as variáveis são declaradas como Person, mesmo quando a instância é um tipo derivado de Student ou Teacher. As instâncias têm as mesmas propriedades e os mesmos valores de propriedade. Mas student == teacher retorna False, embora ambas sejam variáveis do tipo Person, e student == student2 retorna True, embora uma seja uma variável Person e outra seja uma variável Student. O teste de igualdade depende do tipo de runtime do objeto real, não do tipo declarado da variável.

Para implementar esse comportamento, o compilador sintetiza uma propriedade EqualityContract que retorna um objeto Type que corresponde ao tipo do registro. O EqualityContract permite que os métodos de igualdade comparem o tipo de runtime de objetos quando eles estão verificando a igualdade. Se o tipo base de um registro for object, essa propriedade será virtual. Se o tipo base for outro tipo de registro, essa propriedade será uma substituição. Se o tipo de registro for sealed, essa propriedade será efetivamente sealed porque o tipo é sealed.

Quando o código compara duas instâncias de um tipo derivado, os métodos de igualdade sintetizados verificam a igualdade de todos os membros de dados da base e dos tipos derivados. O método GetHashCode sintetizado usa o método GetHashCode de todos os membros de dados declarados no tipo base e no tipo de registro derivado. Os membros de dados de um record incluem todos os campos declarados e o campo de backup sintetizado pelo compilador para quaisquer propriedades autoimplementadas.

Expressões with em registros derivados

O resultado de uma expressão with tem o mesmo tipo em tempo de execução que o operando da expressão, como mostra o exemplo a seguir: Todas as propriedades do tipo de tempo de execução são copiadas, mas você só pode definir propriedades do tipo de tempo de compilação, como mostra o exemplo a seguir:

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 }
}

Formação PrintMembers em registros derivados

O método sintetizado PrintMembers de um tipo de registro derivado chama a implementação base. O resultado é que todas as propriedades públicas e campos de tipos derivados e base estão incluídos na saída ToString, conforme mostrado no exemplo a seguir:

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 }
}

Você pode fornecer sua própria implementação do método PrintMembers. Se fizer isso, use a seguinte assinatura:

  • Para um registro sealed que deriva de object (não declara um registro base): private bool PrintMembers(StringBuilder builder);
  • Para um registro sealed que deriva de outro registro (observe que o tipo delimitador é sealed, portanto, o método é efetivamente sealed): protected override bool PrintMembers(StringBuilder builder);
  • Para um registro que não é sealed e que não deriva de objeto: protected virtual bool PrintMembers(StringBuilder builder);
  • Para um registro que não é sealed e que não deriva de outro registro: protected override bool PrintMembers(StringBuilder builder);

Aqui está um exemplo de código que substitui os métodos sintetizados PrintMembers, um para um tipo de registro que deriva de objeto e outro para um tipo de registro que deriva de outro registro:

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 }
}

Observação

No C# 10 e posterior, o compilador sintetizará PrintMembers em registros derivados mesmo quando um registro base tiver selado o método ToString. Você também pode criar sua própria implementação de PrintMembers.

Comportamento do desconstrutor em registros derivados

O método Deconstruct de um registro derivado retorna os valores de todas as propriedades posicionais do tipo de tempo de compilação. Se o tipo de variável for um registro base, somente as propriedades do registro base serão desconstruídas, a menos que o objeto seja convertido no tipo derivado. O exemplo a seguir demonstra a chamada de um desconstrutor em um registro derivado.

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
}

Restrições genéricas

A palavra-chave record é um modificador para um tipo class ou struct. Adicionar o modificador record inclui o comportamento descrito anteriormente neste artigo. Não há nenhuma restrição genérica que exija que um tipo seja um registro. Um record class satisfaz a restrição class. Um record struct satisfaz a restrição struct. Para obter mais informações, consulte Restrições em parâmetros de tipo.

Especificação da linguagem C#

Para obter mais informações, confira a seção Classes da Especificação da linguagem C#.

Para obter mais informações sobre esses recursos, confira as seguintes notas sobre a proposta de recurso:

Confira também