Criar tipos de registro

Os registros são tipos que usam a igualdade baseada em valor. O C# 10 adiciona estruturas de registro para que você possa definir registros como 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 outros recursos que você provavelmente desejará em tipos de registro. O compilador gera muitos desses membros quando você declara um em vez de um recordclassarquivo . O compilador gera esses mesmos métodos para record struct tipos.

Neste tutorial, irá aprender a:

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

Pré-requisitos

Você precisará configurar sua máquina para executar o .NET 6 ou posterior, incluindo o compilador C# 10 ou posterior. O compilador C# 10 está disponível a partir do Visual Studio 2022 ou do SDK do .NET 6.

Características dos registos

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

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

Você também pode declarar registros posicionais usando uma sintaxe mais concisa. O compilador sintetiza mais métodos para você quando você declara 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 somente de inicialização para record class tipos e readonly record struct tipos. Para record struct tipos, eles são leitura-gravação.
  • Um Deconstruct método para extrair propriedades do registro.

Dados de temperatura da construção

Dados e estatísticas estão entre os cenários em que você vai querer usar registros. Para este tutorial, você criará um aplicativo que calcula os dias de graduação para diferentes usos. Os graus-dias são uma medida de calor (ou falta de calor) durante um período de dias , semanas ou meses. Os graus-dias rastreiam e preveem o uso de energia. Dias mais quentes significam mais ar condicionado e dias mais frios significam mais utilização do forno. Os graus-dias ajudam a gerir as populações de plantas e correlacionam-se com o crescimento das plantas à medida que as estações mudam. Os graus-dias ajudam a rastrear as migrações de animais para espécies que viajam para corresponder ao clima.

A fórmula baseia-se na temperatura média num determinado dia e numa temperatura de base. Para calcular graus-dias 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 DailyTemperature registro é um readonly record struct, porque você não pretende herdar dele, e deve ser imutável. As HighTemp propriedades e LowTemp são init only properties, o que significa que podem ser definidas no construtor ou usando um inicializador de propriedade. Se você quiser que os parâmetros posicionais sejam leitura-gravação, declare um em vez de um record structreadonly record structarquivo . 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 aos registros, incluindo registros posicionais. Você precisará 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 nos certificar de que você pode 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 à exibição a seguir (várias linhas removidas por 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 sintetizado ToString pelo compilador. Se você preferir um texto diferente, você pode escrever sua própria versão que impede que ToString o compilador sintetize uma versão para você.

Calcular dias de grau

Para calcular graus-dias, você tira a diferença de uma temperatura de linha de base e a temperatura média em um determinado dia. Para medir o calor ao longo do tempo, você descarta todos os dias em que a temperatura média está abaixo da linha de base. Para medir o frio ao longo do tempo, você descarta todos os dias em que a temperatura média está acima da linha de base. Por exemplo, os EUA usam 65F como base para graus-dias de aquecimento e resfriamento. Essa é a temperatura em que não é necessário aquecimento ou refrigeração. Se um dia tem uma temperatura média de 70F, esse dia é cinco graus dias de resfriamento e zero graus de aquecimento dias. Por outro lado, se a temperatura média for de 55F, esse dia é de 10 graus dias de aquecimento e 0 graus de resfriamento dias.

Você pode expressar essas fórmulas como uma pequena hierarquia de tipos de registro: um tipo abstrato de dia de grau e dois tipos concretos para graus-dias de aquecimento e graus-dias de resfriamento. Esses tipos também podem ser registros posicionais. Eles tomam uma temperatura de linha de base e uma sequência de registros diários de temperatura 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 HeatingDegreeDays registros e CoolingDegreeDays . As declarações do construtor primário nos registros derivados mostram como gerenciar a inicialização do 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 primários do construtor. 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ê obterá uma saída como a seguinte exibição:

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 graus-dias de aquecimento e resfriamento durante esse período de tempo. Mas este exemplo mostra por que você pode querer 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, testes de System.IEquatable<T> igualdade e desigualdade e GetHashCode(). Para isso, você vai sintetizar PrintMembers. Você também pode declarar o seu próprio 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 registo for , ou um record struct, a assinatura for sealedprivate bool PrintMembers(StringBuilder builder);
  • Se um tipo de registro não for e derivar de object (ou seja, não sealed declarar um registro base), a assinatura seráprotected virtual bool PrintMembers(StringBuilder builder);
  • Se um tipo de registro não sealed for e derivar de outro registro, a assinatura será protected override bool PrintMembers(StringBuilder builder);

Estas regras são mais fáceis de compreender através da compreensão do propósito do PrintMembers. PrintMembers Adiciona informações sobre cada propriedade em um tipo de registro a uma cadeia de caracteres. O contrato exige registros básicos para adicionar seus membros à exibição e pressupõe que os membros derivados adicionarão seus membros. Cada tipo de registro sintetiza uma ToString substituição 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 PrintMembers método no DegreeDays registro 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ê errar os acessadores; o idioma 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 correta.

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

Mutação não destrutiva

Os membros sintetizados em uma classe de registro posicional não modificam o estado do registro. O objetivo é que você possa criar registros imutáveis com mais facilidade. Lembre-se de que você declara um readonly record struct para criar uma struct de registro imutável. Analise novamente as declarações anteriores para HeatingDegreeDays e CoolingDegreeDays. Os membros adicionados executam cálculos nos valores para o registro, mas não mudam de 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ê vai querer usar mutações não destrutivas. Você cria novas instâncias de registro que são 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 registo original mantém-se inalterado.

Vamos adicionar alguns recursos ao seu programa que demonstram with expressões. Primeiro, vamos criar um novo registro para calcular graus-dias crescentes usando os mesmos dados. Os graus-dias de crescimento normalmente usam 41F como linha de base e medem 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 computados com os números gerados com uma temperatura de base mais alta. Lembre-se de que os registros são tipos de referência e essas cópias são cópias superficiais. A matriz dos dados não é copiada, mas ambos os registros se referem aos mesmos dados. Esse facto é uma vantagem num outro cenário. Para os dias de grau, é útil acompanhar 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 dessas acumulações e, em seguida, 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 with expressão. 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 aspetos dos registros. Os registros fornecem sintaxe concisa para tipos em que o uso fundamental é o armazenamento de dados. Para classes orientadas a objetos, o uso fundamental é definir responsabilidades. Este tutorial concentrou-se em registros posicionais, onde 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. Pode adicionar quaisquer outros membros de que necessite para os seus tipos de registo. Você pode criar tipos de registro imutáveis sabendo que nenhum dos membros gerados pelo compilador mudaria de estado. E with as expressões facilitam o suporte a mutações não destrutivas.

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 record struct tipos quando deseja os recursos de registros para um tipo que é pequeno o suficiente para copiar com eficiência.

Você pode saber mais sobre registros no artigo de referência da linguagem C# para o tipo de registro e a especificação de tipo de registro proposta e a especificação de estrutura de registro.