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 tipoclass
. - 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
):
- Uma substituição de Object.Equals(Object).
- Um método de
Equals
virtual cujo parâmetro é o tipo de registro. - Uma substituição de Object.GetHashCode().
- Métodos para
operator ==
eoperator !=
. - Os tipos de registro implementam System.IEquatable<T>.
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
ereadonly record struct
tipos. Pararecord 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
sealed
ou umrecord struct
, a assinatura seráprivate bool PrintMembers(StringBuilder builder);
- Se um tipo de registro não for
sealed
e derivar deobject
(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 .