Criar tipos de registro

Os registros são tipos que usam igualdade baseada em valor. Você pode definir registros como tipos de referência ou tipos de valor. Duas variáveis de um tipo de registro são iguais se as definições de tipo de registro forem idênticas e, se para cada campo, os valores em ambos os registros forem iguais. Duas variáveis de um tipo de classe são iguais se os objetos referidos forem do mesmo tipo de classe e as variáveis se referirem ao mesmo objeto. A igualdade baseada em valor implica outras funcionalidades que você provavelmente deseja em tipos de registro. O compilador gera muitos desses membros quando você declara um record em vez de um class. O compilador gera esses mesmos métodos para record struct tipos.

Neste tutorial, você aprenderá como:

  • Decida se você adiciona o modificador record a um tipo class.
  • Declare tipos de registro e tipos de registro posicional.
  • Substitua seus métodos por métodos gerados pelo compilador em registros.

Pré-requisitos

Características dos registros

Você define um registro ao declarar um tipo com a palavra-chave record, modificando uma declaração class ou struct. Opcionalmente, você pode omitir a class palavra-chave para criar um record class. Um registro segue a semântica de igualdade baseada em valor. Para impor semântica de valor, o compilador gera vários métodos para o tipo de registro (para tipos record class e record struct):

Os registros também fornecem uma substituição de Object.ToString(). O compilador sintetiza métodos para exibir registros usando Object.ToString(). Você explora esses membros enquanto escreve o código para este tutorial. Os registros dão suporte a expressões with para habilitar a mutação não destrutiva de registros.

Você também pode declarar registros posicionais usando uma sintaxe mais concisa. O compilador sintetiza mais métodos para você ao declarar registros posicionais:

  • Um construtor primário cujos parâmetros correspondem aos parâmetros posicionais na declaração de registro.
  • Propriedades públicas para cada parâmetro de um construtor primário. Essas propriedades são apenas para inicialização para tipos record class e readonly record struct. Para tipos record struct, elas são leitura-gravação.
  • Um Deconstruct método para extrair propriedades do registro.

Compilar dados de temperatura

Dados e estatísticas estão entre os cenários em que você deseja usar registros. Para este tutorial, você criará um aplicativo que computa dias de grau para usos diferentes. Os dias de grau são uma medida de calor (ou falta de calor) durante um período de dias, semanas ou meses. Os dias de grau acompanham e prevêem o uso de energia. Dias mais quentes significam mais ar condicionado, e dias mais frios significam mais uso de fornos. Os dias de grau ajudam a gerenciar as populações vegetais e a correlacionar-se com o crescimento das plantas à medida que as estações mudam. Os dias de grau ajudam a acompanhar as migrações de animais para espécies que viajam para corresponder ao clima.

A fórmula é baseada na temperatura média em um determinado dia e em uma temperatura de linha de base. Para calcular dias de grau ao longo do tempo, você precisará da temperatura alta e baixa todos os dias por um período de tempo. Vamos começar criando um novo aplicativo. Crie um novo aplicativo de console. Crie um novo tipo de registro em um novo arquivo chamado "DailyTemperature.cs":

public readonly record struct DailyTemperature(double HighTemp, double LowTemp);

O código anterior define um registro posicional. O registro DailyTemperature é um readonly record struct, porque você não pretende herdar dele, e deve ser imutável. As HighTemp e LowTemp são propriedades de inicialização somente, o que significa que só podem ser definidas no construtor ou usando um inicializador de propriedade. Se você quiser que os parâmetros posicionais sejam de leitura e escrita, declare um record struct em vez de um readonly record struct. O DailyTemperature tipo também tem um construtor primário que tem dois parâmetros que correspondem às duas propriedades. Use o construtor primário para inicializar um DailyTemperature registro. O código a seguir cria e inicializa vários DailyTemperature registros. O primeiro usa parâmetros nomeados para esclarecer o HighTemp e LowTemp. Os inicializadores restantes usam parâmetros posicionais para inicializar o HighTemp e LowTemp:

private static DailyTemperature[] data = [
    new DailyTemperature(HighTemp: 57, LowTemp: 30), 
    new DailyTemperature(60, 35),
    new DailyTemperature(63, 33),
    new DailyTemperature(68, 29),
    new DailyTemperature(72, 47),
    new DailyTemperature(75, 55),
    new DailyTemperature(77, 55),
    new DailyTemperature(72, 58),
    new DailyTemperature(70, 47),
    new DailyTemperature(77, 59),
    new DailyTemperature(85, 65),
    new DailyTemperature(87, 65),
    new DailyTemperature(85, 72),
    new DailyTemperature(83, 68),
    new DailyTemperature(77, 65),
    new DailyTemperature(72, 58),
    new DailyTemperature(77, 55),
    new DailyTemperature(76, 53),
    new DailyTemperature(80, 60),
    new DailyTemperature(85, 66) 
];

Você pode adicionar suas próprias propriedades ou métodos a registros, incluindo registros posicionais. Você precisa calcular a temperatura média para cada dia. Você pode adicionar essa propriedade ao DailyTemperature registro:

public readonly record struct DailyTemperature(double HighTemp, double LowTemp)
{
    public double Mean => (HighTemp + LowTemp) / 2.0;
}

Vamos garantir que você possa usar esses dados. Adicione o seguinte código ao seu Main método:

foreach (var item in data)
    Console.WriteLine(item);

Execute seu aplicativo e você verá uma saída semelhante à seguinte exibição (várias linhas removidas para espaço):

DailyTemperature { HighTemp = 57, LowTemp = 30, Mean = 43.5 }
DailyTemperature { HighTemp = 60, LowTemp = 35, Mean = 47.5 }


DailyTemperature { HighTemp = 80, LowTemp = 60, Mean = 70 }
DailyTemperature { HighTemp = 85, LowTemp = 66, Mean = 75.5 }

O código anterior mostra a saída da substituição de ToString sintetizado pelo compilador. Se preferir texto diferente, você poderá escrever sua própria versão ToString que impeça o compilador de sintetizar uma versão para você.

Computar dias de grau

Para computar dias de grau, você faz a diferença de uma temperatura de linha de base e da temperatura média em um determinado dia. Para medir o calor ao longo do tempo, você descarta qualquer dia em que a temperatura média esteja abaixo da linha de base. Para medir o frio ao longo do tempo, você descarta qualquer dia em que a temperatura média esteja acima da linha de base. Por exemplo, os EUA usam 65 F como base para dias de aquecimento e grau de resfriamento. Essa é a temperatura onde nenhum aquecimento ou resfriamento é necessário. Se um dia apresenta uma temperatura média de 70 °F, esse dia representa cinco graus-dia de resfriamento e zero graus-dia de aquecimento. Por outro lado, se a temperatura média for de 55 F, esse dia é 10 dias de aquecimento e 0 dias de resfriamento.

Você pode expressar essas fórmulas como uma pequena hierarquia de tipos de registro: um tipo de dia de grau abstrato e dois tipos concretos para dias de grau de aquecimento e dias de grau de resfriamento. Esses tipos também podem ser registros posicionais. Eles obtêm uma temperatura de linha de base e uma sequência de registros de temperatura diária como argumentos para o construtor primário:

public abstract record DegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords);

public sealed record HeatingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean < BaseTemperature).Sum(s => BaseTemperature - s.Mean);
}

public sealed record CoolingDegreeDays(double BaseTemperature, IEnumerable<DailyTemperature> TempRecords)
    : DegreeDays(BaseTemperature, TempRecords)
{
    public double DegreeDays => TempRecords.Where(s => s.Mean > BaseTemperature).Sum(s => s.Mean - BaseTemperature);
}

O registro abstrato DegreeDays é a classe base compartilhada para os registros HeatingDegreeDays e CoolingDegreeDays. As declarações do construtor primário nos registros derivados mostram como gerenciar a inicialização de registro base. Seu registro derivado declara parâmetros 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. Neste exemplo, os registros derivados não adicionam novos parâmetros de construtor primário. Teste seu código adicionando o seguinte código ao seu Main método:

var heatingDegreeDays = new HeatingDegreeDays(65, data);
Console.WriteLine(heatingDegreeDays);

var coolingDegreeDays = new CoolingDegreeDays(65, data);
Console.WriteLine(coolingDegreeDays);

Você obtém um resultado semelhante ao exemplo a seguir:

HeatingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 85 }
CoolingDegreeDays { BaseTemperature = 65, TempRecords = record_types.DailyTemperature[], DegreeDays = 71.5 }

Definir métodos sintetizados pelo compilador

Seu código calcula o número correto de dias de aquecimento e resfriamento durante esse período de tempo. Mas este exemplo mostra por que talvez você queira substituir alguns dos métodos sintetizados para registros. Você pode declarar sua própria versão de qualquer um dos métodos sintetizados pelo compilador em um tipo de registro, exceto o método clone. O método clone tem um nome gerado pelo compilador e você não pode fornecer uma implementação diferente. Esses métodos sintetizados incluem um construtor de cópia, os membros da interface System.IEquatable<T>, os testes de igualdade e desigualdade, e GetHashCode(). Para essa finalidade, você sintetiza PrintMembers. Você também pode declarar sua própria ToString, mas PrintMembers fornece uma opção melhor para cenários de herança. Para fornecer sua própria versão de um método sintetizado, a assinatura deve corresponder ao método sintetizado.

O TempRecords elemento na saída do console não é útil. Ele exibe o tipo, mas nada mais. Você pode alterar esse comportamento fornecendo sua própria implementação do método sintetizado PrintMembers . A assinatura depende dos modificadores aplicados à record declaração:

  • Se um tipo de registro for sealed, ou um record struct, a assinatura será private bool PrintMembers(StringBuilder builder);
  • Se um tipo de registro não é sealed e deriva de object (ou seja, ele não declara um registro base), a assinatura é protected virtual bool PrintMembers(StringBuilder builder);
  • Se um tipo de registro não for sealed e deriva de outro registro, a assinatura será protected override bool PrintMembers(StringBuilder builder);

Essas regras são mais fáceis de compreender por meio da compreensão da finalidade de PrintMembers. PrintMembers adiciona informações sobre cada propriedade em um tipo de registro a uma cadeia de caracteres. O contrato exige que os registros básicos adicionem seus membros à exibição e pressupõe que os membros derivados adicionem seus membros. Cada tipo de registro sintetiza uma substituição ToString semelhante ao exemplo a seguir para HeatingDegreeDays:

public override string ToString()
{
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.Append("HeatingDegreeDays");
    stringBuilder.Append(" { ");
    if (PrintMembers(stringBuilder))
    {
        stringBuilder.Append(" ");
    }
    stringBuilder.Append("}");
    return stringBuilder.ToString();
}

Você declara um método PrintMembers no registro DegreeDays que não imprime o tipo da coleção:

protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
    stringBuilder.Append($"BaseTemperature = {BaseTemperature}");
    return true;
}

A assinatura declara um virtual protected método para corresponder à versão do compilador. Não se preocupe se você tiver errado os acessadores; a linguagem impõe a assinatura correta. Se você esquecer os modificadores corretos para qualquer método sintetizado, o compilador emitirá avisos ou erros que ajudam você a obter a assinatura certa.

Você pode declarar o ToString método como sealed em um tipo de registro. Isso impede que registros derivados forneçam uma nova implementação. Os registros derivados ainda conterão a substituição PrintMembers. Você selaria ToString se não quisesse que ele exibisse o tipo de runtime do registro. No exemplo anterior, você perderia as informações sobre onde o registro estava medindo dias de aquecimento ou grau de resfriamento.

Mutação não destrutiva

Os membros sintetizados em uma classe de registro posicional não modificam o estado do registro. A meta é que você possa criar registros imutáveis com mais facilidade. Lembre-se de declarar um readonly record struct para criar um struct de registro imutável. Examine novamente as declarações anteriores para HeatingDegreeDays e CoolingDegreeDays. Os membros adicionados executam cálculos nos valores do registro, mas não modificam o estado. Os registros posicionais facilitam a criação de tipos de referência imutáveis.

Criar tipos de referência imutáveis significa que você deseja usar mutação não estruturativa. Você cria novas instâncias de registro semelhantes às instâncias de registro existentes usando with expressões. Essas expressões são uma construção de cópia com atribuições adicionais que modificam a cópia. O resultado é uma nova instância de registro em que cada propriedade foi copiada do registro existente e, opcionalmente, modificada. O registro original não foi alterado.

Vamos adicionar algumas funcionalidades ao seu programa que demonstram expressões with. Primeiro, vamos criar um novo registro para calcular dias de grau crescente usando os mesmos dados. Dias de grau crescente normalmente usam 41 F como a linha de base e mede as temperaturas acima da linha de base. Para usar os mesmos dados, você pode criar um novo registro semelhante ao coolingDegreeDays, mas com uma temperatura base diferente:

// Growing degree days measure warming to determine plant growing rates
var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };
Console.WriteLine(growingDegreeDays);

Você pode comparar o número de graus calculados com os números gerados com uma temperatura de linha de base mais alta. Lembre-se de que os registros são tipos de referência e essas cópias são cópias rasas. A matriz dos dados não é copiada, mas ambos os registros se referem aos mesmos dados. Esse fato é uma vantagem em outro cenário. Para dias de grau crescente, é útil controlar o total dos cinco dias anteriores. Você pode criar novos registros com dados de origem diferentes usando with expressões. O código a seguir cria uma coleção desses acúmulos e exibe os valores:

// showing moving accumulation of 5 days using range syntax
List<CoolingDegreeDays> movingAccumulation = new();
int rangeSize = (data.Length > 5) ? 5 : data.Length;
for (int start = 0; start < data.Length - rangeSize; start++)
{
    var fiveDayTotal = growingDegreeDays with { TempRecords = data[start..(start + rangeSize)] };
    movingAccumulation.Add(fiveDayTotal);
}
Console.WriteLine();
Console.WriteLine("Total degree days in the last five days");
foreach(var item in movingAccumulation)
{
    Console.WriteLine(item);
}

Você também pode usar with expressões para criar cópias de registros. Não especifique nenhuma propriedade entre as chaves para a expressão with. Isso significa criar uma cópia e não alterar nenhuma propriedade:

var growingDegreeDaysCopy = growingDegreeDays with { };

Execute o aplicativo concluído para ver os resultados.

Resumo

Este tutorial mostrou vários aspectos dos registros. Os registros fornecem sintaxe concisa para tipos em que o uso fundamental é armazenar dados. Para classes orientadas a objetos, o uso fundamental é definir responsabilidades. Este tutorial se concentrou em registros posicionais, em que você pode usar uma sintaxe concisa para declarar as propriedades de um registro. O compilador sintetiza vários membros do registro para copiar e comparar registros. Você pode adicionar quaisquer outros membros que precisar para seus tipos de registro. Você pode criar tipos de registro imutáveis sabendo que nenhum dos membros gerados pelo compilador alteraria o estado. E with as expressões facilitam o apoio à mutação não destrutiva.

Os registros adicionam outra maneira de definir tipos. Você usa class definições para criar hierarquias orientadas a objetos que se concentram nas responsabilidades e no comportamento dos objetos. Você cria struct tipos para estruturas de dados que armazenam dados e são pequenas o suficiente para copiar com eficiência. Você cria record tipos quando deseja igualdade e comparação baseadas em valor, não deseja copiar valores e deseja usar variáveis de referência. Você cria tipos record struct quando deseja os recursos de registros para um tipo pequeno o suficiente para copiar com eficiência.

Você pode saber mais sobre registros no artigo de referência de linguagem C# para o tipo de registro, na especificação de tipo de registro e na especificação do struct de registro.