Partilhar via


Criar tipos de registro

Registos são tipos que utilizam 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 outros recursos 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 exatamente os mesmos métodos para tipos record struct.

Neste tutorial, você aprenderá a:

  • Decida se deve adicionar 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

Você precisa configurar sua máquina para executar o .NET 6 ou posterior. O compilador C# está disponível com o Visual Studio 2022 ou o .NET SDK.

Características dos registos

Você define um registro declarando um tipo com a palavra-chave record, modificando uma declaração class ou struct. Opcionalmente, você pode omitir a palavra-chave class para criar um record class. 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 de record class quanto para tipos de 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 suportam expressões with para permitir 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ê 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 tipos record class e readonly record struct tipos. Para record struct tipos, eles são leitura-gravação.
  • Um método Deconstruct para extrair propriedades do registro.

Criar dados de temperatura

Dados e estatísticas estão entre os cenários em que você deseja usar registros. Para este tutorial, você cria um aplicativo que calcula dias de grau para diferentes usos. Graus-dias são uma medida de calor (ou ausência dele) durante um período de dias, semanas ou meses. Os graus-dias rastreiam e prevêem o consumo 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 espécies animais que viajam para se adaptarem 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 registro DailyTemperature é um readonly record struct, porque você não pretende herdar dele, e ele deve ser imutável. As propriedades HighTemp e LowTemp são init only properties, o que significa que podem ser definidas no construtor ou usando um inicializador de propriedade. Se quiseres que os parâmetros posicionais sejam de leitura e gravação, declara um record struct em vez de um readonly record struct. O tipo DailyTemperature também tem um construtor primário com dois parâmetros que correspondem às duas propriedades. Você usa o construtor primário para inicializar um registo DailyTemperature. O código a seguir cria e inicializa vários registros DailyTemperature. 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ê precisa calcular a temperatura média para cada dia. Você pode adicionar essa propriedade ao registro DailyTemperature:

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 método Main:

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 ToString sintetizada pelo compilador. Se você preferir texto diferente, você pode escrever sua própria versão do ToString que impede o compilador de sintetizar uma versão para você.

Calcular dias de grau

Para calcular graus-dias, calcula-se a diferença entre uma temperatura de referência e a temperatura média de 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 65 F como base para dias de grau 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 70 F, esse dia tem cinco graus-dia de arrefecimento e zero graus-dia de aquecimento. Por outro lado, se a temperatura média fosse 55 Fahrenheit, esse dia seria de 10 dias de aquecimento e 0 dias de arrefecimento.

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 DegreeDays abstrato é 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 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 método Main:

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

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

Você obtém uma saída como a exibida no ecrã:

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 System.IEquatable<T>, os testes de igualdade e desigualdade, e GetHashCode(). Para isso, você sintetiza PrintMembers. Você também pode declarar o seu próprio ToString, mas PrintMembers oferece 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 elemento TempRecords na saída do console não é útil. Ele apresenta o tipo, mas nada mais. Você pode alterar esse comportamento fornecendo sua própria implementação do método PrintMembers sintetizado. A assinatura depende dos modificadores aplicados à declaração record:

  • Se um tipo de registro for sealedou um record struct, a assinatura será private bool PrintMembers(StringBuilder builder);
  • Se um tipo de registro não for sealed e derivar de object (ou seja, não declarar um registro base), a assinatura será protected virtual bool PrintMembers(StringBuilder builder);
  • Se um tipo de registro não for sealed 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 de PrintMembers. PrintMembers adiciona informações sobre cada propriedade em um tipo de registro a uma cadeia de caracteres. O contrato exige que os registos base adicionem os seus membros à apresentação e pressupõe que os membros derivados adicionem os seus membros. Cada tipo de registro sintetiza uma substituição de ToString que se parece com o 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 método virtual protected para corresponder à versão do compilador. Não se preocupe se errar nos métodos de acesso; 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 correta.

Você pode declarar o método ToString como sealed em um tipo de registro. Isso impede que os registros derivados forneçam uma nova implementação. Os registos derivados ainda conterão o override de PrintMembers. 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 uma readonly record struct para criar uma estrutura de registro imutável. Analise novamente as declarações anteriores para HeatingDegreeDays e CoolingDegreeDays. Os membros adicionados executam cálculos sobre os valores do registo, 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ê deseja usar mutações não destrutivas. Você cria novas instâncias de registro que são semelhantes às instâncias de registro existentes usando expressões with. Essas expressões são uma construção de cópia com atribuições extras 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 um par de recursos ao seu programa que demonstram expressões with. Primeiro, vamos criar um novo registo para computar graus-dia acumulados usando os mesmos dados. Graus-dias de crescimento normalmente usa 41 F como linha de base e mede 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 soma térmica, é útil acompanhar o total dos últimos cinco dias. Você pode criar novos registros com dados de origem diferentes usando expressões with. 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 expressões with para criar cópias de registros. Não especifique propriedades entre as chaves na 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 aspetos dos registos. 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. As with expressões tornam fácil o suporte a mutações não destrutivas.

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

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