Registros (referência C#)

Use o record modificador para definir um tipo de referência que forneça funcionalidade interna para encapsular dados. C# 10 permite que a record class sintaxe como 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 primários do construtor para um registro são referidos como parâmetros posicionais. O compilador cria propriedades posicionais que espelham o construtor primário ou parâmetros posicionais. O compilador não sintetiza propriedades para parâmetros primários do construtor em tipos que não têm o record modificador.

Os dois exemplos seguintes demonstram record (ou record class) tipos de referência:

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 record struct tipos de valor:

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

As estruturas de registro também podem ser mutáveis, tanto as estruturas de registro posicional quanto as estruturas 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 ao suporte a modelos de dados imutáveis. O tipo de registo oferece as seguintes características:

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

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

O restante deste artigo discute ambos e record classrecord struct tipos. 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. Ou record structrecord class é usado para descrever o comportamento que se aplica apenas aos tipos struct ou class, respectivamente. O record struct tipo 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 autoimplementada para cada parâmetro posicional fornecido na declaração de registro.
    • Para record tipos e readonly record struct tipos: Uma propriedade somente init.
    • Para record struct tipos: Uma propriedade de leitura-gravação.
  • Um construtor primário cujos parâmetros correspondem aos parâmetros posicionais na declaração de registro.
  • Para tipos struct de registro, um construtor sem parâmetros que define cada campo com seu valor padrão.
  • Um Deconstruct método com um out parâmetro para cada parâmetro posicional fornecido na declaração de registo. O método desconstrói propriedades definidas usando sintaxe posicional; ele ignora as propriedades que são definidas usando a sintaxe de propriedade padrão.

Você pode querer 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 o System.Text.Json.Serialization.JsonPropertyNameAttribute a cada propriedade do Person registro. O property: destino indica que o atributo é aplicado à propriedade gerada pelo compilador. Outros valores são field: aplicar o atributo ao campo e param: 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 <param> tag para adicionar documentação para os parâmetros do construtor primário.

Se a definição de propriedade autoimplementada gerada não for a desejada, você poderá definir sua própria propriedade com o mesmo nome. Por exemplo, você pode querer alterar a acessibilidade ou mutabilidade, ou fornecer uma implementação para o get ou set acessador. Se você declarar a propriedade em sua origem, deverá inicializá-la a partir do parâmetro posicional do registro. Se sua propriedade for uma propriedade implementada automaticamente, você deverá inicializá-la. Se você adicionar um campo de suporte 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 FirstName propriedades e LastName de um registro publicposicional, mas restringe o Id parâmetro posicional a internal. Você pode usar essa sintaxe para registros e tipos de estrutura 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 quaisquer propriedades posicionais 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 registro somente leitura posicional struct declaram propriedades somente init. Uma struct de registro posicional declara propriedades de leitura-gravação. 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 thread-safe ou você está dependendo de um código hash permanecer 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 oferece suporte à atualização com tipos de entidade imutáveis.

As propriedades somente de inicialização, sejam criadas a partir de parâmetros posicionais (record class, e readonly record struct) ou especificando init acessadores, têm imutabilidade superficial. Após a inicialização, não é possível alterar o valor das propriedades de tipo de 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 do tipo de referência (uma matriz, neste 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 modificando o estado do objeto. A menos que especificado, os métodos sintetizados são gerados para record, record structe readonly record struct declarações.

Igualdade de valores

Se você não substituir ou substituir métodos de igualdade, o tipo declarado governará como a igualdade é definida:

  • Para class tipos, dois objetos são iguais se se referirem ao mesmo objeto na memória.
  • Para struct tipos, dois objetos são iguais se forem do mesmo tipo e armazenarem os mesmos valores.
  • Para tipos com o record modificador (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 que para um struct. A diferença é que, para um struct, a implementação está em ValueType.Equals(Object) e depende de 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, registros e estruturas 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 valor 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 vários métodos, incluindo:

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

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

  • A virtual, ou sealed, Equals(R? other) onde R é o tipo de registo. Este método implementa IEquatable<T>. Este método pode ser declarado explicitamente.

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

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

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

  • Se o tipo de registo for derivado de um tipo de registo base, protected override Type EqualityContract { get; };. Esta 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 para obter uma with mutação não destrutiva. Uma with expressão cria 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 objeto 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 with expressão pode definir propriedades posicionais ou propriedades criadas usando sintaxe de propriedade padrão. As propriedades declaradas explicitamente devem ter um init acessador ou set para serem alteradas em uma with expressão.

O resultado de uma with expressão é uma cópia superficial, o que significa que, para uma propriedade de referência, apenas a referência a uma instância é copiada. Tanto o registo original como a cópia acabam por ter uma referência ao mesmo caso.

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

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

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

Formatação integrada para exibição

Os tipos de registro têm um método gerado pelo ToString compilador que exibe os nomes e valores de propriedades e campos públicos. O ToString método 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, onde ToString retorna System.String[]:

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

Para implementar esse recurso, em record class tipos, o compilador sintetiza um método virtual PrintMembers e uma ToString substituição. Em record struct tipos, este membro é private. A ToString substituição cria um StringBuilder objeto com o nome do tipo seguido por um colchete de abertura. Ele chama PrintMembers para adicionar nomes de propriedade e valores e, em seguida, adiciona o colchete de fechamento. O exemplo a seguir mostra um 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 ou PrintMembers a ToString substituição. Exemplos são fornecidos na seção formatação em registros derivados, mais adiante PrintMembers neste artigo. Em C# 10 e posterior, sua implementação de ToString pode incluir o sealed modificador, o que impede que o compilador sintetize uma ToString implementação para quaisquer registros derivados. Você pode criar uma representação de cadeia de caracteres consistente em toda uma hierarquia de record tipos. (Os registros derivados ainda têm um PrintMembers método gerado para todas as propriedades derivadas.)

Herança

Esta secção aplica-se apenas aos record class tipos.

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 de posição em tipos de registo 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 sucessórias

Esta seção se aplica a tipos, mas não record struct a record class tipos. Para que duas variáveis de registro sejam iguais, o tipo de tempo de execução deve ser igual. Os tipos das variáveis que contêm podem ser diferentes. A comparação da 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 um 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 Personvariáveis do tipo -, e student == student2 retorna True embora uma seja uma Person variável e uma seja uma Student variável. O teste de igualdade depende do tipo de tempo de execução do objeto real, não do tipo declarado da variável.

Para implementar esse comportamento, o compilador sintetiza uma EqualityContract propriedade que retorna um Type objeto que corresponde ao tipo do registro. O EqualityContract permite que os métodos de igualdade comparem o tipo de tempo de execução 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 todos os membros de dados da base e tipos derivados para igualdade. O método sintetizado GetHashCode usa o GetHashCode método 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 suporte sintetizado pelo compilador para quaisquer propriedades implementadas automaticamente.

with expressões em registos derivados

O resultado de uma with expressão tem o mesmo tipo de tempo de execução que o operando da expressão. 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 }
}

PrintMembers formatação 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 dos tipos derivado e base são incluídos na ToString saída, 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 PrintMembers método. Se você fizer isso, use a seguinte assinatura:

  • Para um sealed registro que deriva de object (não declara um registro base): private bool PrintMembers(StringBuilder builder);
  • Para um sealed registro que deriva de outro registro (observe que o tipo de anexo é sealed, portanto, o método é efetivamente sealed): protected override bool PrintMembers(StringBuilder builder);
  • Para um registro que não sealed é e deriva do objeto: protected virtual bool PrintMembers(StringBuilder builder);
  • Para um registro que não sealed é e 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 }
}

Nota

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

Comportamento do desconstrutor em registros derivados

O Deconstruct método 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 para o tipo derivado. O exemplo a seguir demonstra chamar 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 record palavra-chave é um modificador para um class ou struct tipo. Adicionar o record modificador inclui o comportamento descrito anteriormente neste artigo. Não há nenhuma restrição genérica que exija que um tipo seja um registro. A record class satisfaz a class restrição. A record struct satisfaz a struct restrição. Para obter mais informações, consulte Restrições em parâmetros de tipo.

Especificação da linguagem C#

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

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

Consulte também