Compartir a través de


Creación de tipos de registro

Los registros son tipos que usan igualdad basada en valores. Puede definir registros como tipos de referencia o tipos de valor. Dos variables de un tipo de registro son iguales si las definiciones de tipo de registro son idénticas y, si para cada campo, los valores de ambos registros son iguales. Dos variables de un tipo de clase son iguales si los objetos a los que se hace referencia son del mismo tipo de clase y las variables hacen referencia al mismo objeto. La igualdad basada en valores implica otras funcionalidades que probablemente desee en los tipos de registro. El compilador genera muchos de esos miembros al declarar un record en lugar de un class. El compilador genera esos mismos métodos para los tipos de record struct.

En este tutorial, aprenderá a:

  • Decida si agrega el modificador record a un tipo class.
  • Declarar tipos de registro y tipos de registro posicionales.
  • Reemplace sus métodos con los generados por el compilador en los registros.

Prerrequisitos

Debe configurar la máquina para ejecutar .NET 6 o posterior. El compilador de C# está disponible con de Visual Studio 2022 o el SDK de .NET de .

Características de los registros

Para definir un registro , se debe declarar un tipo con la palabra clave record y modificar una declaración class o struct. Opcionalmente, puede omitir la palabra clave class para crear un record class. Un registro sigue la semántica de igualdad basada en valores. Para aplicar la semántica de valores, el compilador genera varios métodos para el tipo de registro (tanto para tipos de record class como para tipos de record struct):

Los registros también proporcionan una invalidación de Object.ToString(). El compilador sintetiza métodos para mostrar registros mediante Object.ToString(). Exploras esos miembros mientras escribes el código para este tutorial. Los registros admiten expresiones with para habilitar la mutación no destructiva de los registros.

También puede declarar registros posicionales mediante una sintaxis más concisa. El compilador sintetiza más métodos para usted al declarar registros posicionales:

  • Constructor principal cuyos parámetros coinciden con los parámetros posicionales en la declaración de registro.
  • Propiedades públicas para cada parámetro de un constructor primario. Estas propiedades son de solo inicialización para los tipos record class y readonly record struct. Para los tipos record struct, son de lectura y escritura.
  • Método Deconstruct para extraer propiedades del registro.

Creación de datos de temperatura

Los datos y las estadísticas se encuentran entre los escenarios en los que desea usar registros. En este tutorial va a compilar una aplicación que calcula grados día para distintos usos. Los grados día son una medida de calor (o falta de él) a lo largo de un período de días, semanas o meses. Los grados día realizan un seguimiento del uso energético y lo predicen. Los días más calientes significan más aire acondicionado, y más días más fríos significan más uso del horno. Los grados día resultan útiles para administrar la población vegetal y poner en correlación su crecimiento a medida que cambiamos de estación. Los grados día ayudan a realizar un seguimiento de las migraciones animales de especies que se desplazan según el clima.

La fórmula se basa en la temperatura media en un día determinado y una temperatura de línea base. Para calcular los grados día a lo largo del tiempo, necesita la temperatura mínima y máxima de cada día durante un período de tiempo. Comencemos creando una nueva aplicación. Cree una nueva aplicación de consola. Cree un nuevo tipo de registro en un nuevo archivo denominado "DailyTemperature.cs":

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

El código anterior define un registro posicional . El registro DailyTemperature es un objeto readonly record struct, porque el objeto no se hereda de él y debe ser inmutable. Las propiedades HighTemp y LowTemp son propiedades de solo inicialización, lo que significa que se pueden establecer en el constructor o mediante un inicializador de propiedad. Si desea que los parámetros posicionales sean de lectura y escritura, declara un record struct en lugar de un readonly record struct. El tipo DailyTemperature también tiene un constructor principal que tiene dos parámetros que coinciden con las dos propiedades. Use el constructor primario para inicializar un registro DailyTemperature. El código siguiente crea e inicializa varios registros DailyTemperature. La primera usa parámetros con nombre para aclarar el HighTemp y LowTemp. Los inicializadores restantes usan parámetros posicionales para inicializar el HighTemp y 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) 
];

Puede agregar sus propias propiedades o métodos a los registros, incluidos los registros posicionales. Debe calcular la temperatura media para cada día. Puede agregar esa propiedad al registro DailyTemperature:

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

Asegúrese de que puede usar estos datos. Agregue el código siguiente al método Main:

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

Ejecute la aplicación y verá una salida similar a la siguiente visualización (se han quitado varias filas para el espacio):

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 }

El código anterior muestra el resultado de la invalidación de ToString sintetizada por el compilador. Si prefiere texto diferente, puede escribir su propia versión de ToString que impida que el compilador sintele una versión.

Cálculo de grados día

Para calcular los grados-día, toma la diferencia entre una temperatura base y la temperatura media en un día determinado. Para medir el calor a lo largo del tiempo, se descartan los días en los que la temperatura media está por debajo de la línea base. Para medir el frío con el tiempo, descarte los días en los que la temperatura media está por encima de la línea base. Por ejemplo, EE. UU. usa 65 °F como base para los días de grados de calefacción y de refrigeración. Esa es la temperatura en la que no se necesita calefacción ni refrigeración. Si un día tiene una temperatura media de 70 F, ese día es de cinco días de enfriamiento y cero días de grado de calentamiento. Por el contrario, si la temperatura media es 55 F, ese día es de 10 días de calefacción y 0 días de enfriamiento.

Estas fórmulas se pueden expresar como una pequeña jerarquía de tipos de registros: un tipo de grado día abstracto y dos tipos concretos de grados días de calefacción y grados día de refrigeración. Estos tipos también pueden ser registros posicionales. Toman una temperatura de línea base y una secuencia de registros de temperatura diarios como argumentos para el constructor principal:

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

El registro de DegreeDays abstracto es la clase base compartida para los registros HeatingDegreeDays y CoolingDegreeDays. Las declaraciones del constructor principal en los registros derivados muestran cómo administrar la inicialización del registro base. El registro derivado declara parámetros para todos los parámetros del constructor principal del registro base. El registro base declara e inicializa esas propiedades. El registro derivado no los oculta, sino que solo crea e inicializa propiedades para los parámetros que no se declaran en su registro base. En este ejemplo, los registros derivados no agregan nuevos parámetros de constructor principal. Pruebe el código agregando el código siguiente al método Main:

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

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

Obtiene una salida parecida a la pantalla que se muestra a continuación.

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

Definición de métodos sintetizados por el compilador

El código calcula el número correcto de días de grado de calefacción y refrigeración durante ese período de tiempo. Pero en este ejemplo se muestra por qué es posible que quiera reemplazar algunos de los métodos sintetizados para los registros. Puede declarar su propia versión de cualquiera de los métodos sintetizados por el compilador en un tipo de registro, excepto el método clone. El método clone tiene un nombre generado por el compilador y no puede proporcionar una implementación diferente. Estos métodos sintetizados incluyen un constructor de copias, los miembros de la interfaz System.IEquatable<T>, las pruebas de igualdad y desigualdad y GetHashCode(). Para ello, sintetiza PrintMembers. También puede declarar su propio elemento ToString, pero PrintMembers constituye una mejor opción para los escenarios de herencia. Para proporcionar su propia versión de un método sintetizado, la firma debe coincidir con el método sintetizado.

El elemento TempRecords del resultado de la consola no es útil. Muestra el tipo, pero no muestra nada más. Puede cambiar este comportamiento proporcionando su propia implementación del método PrintMembers sintetizado. La firma depende de los modificadores aplicados a la declaración record:

  • Si un tipo de registro es sealed, o record struct, la signatura es private bool PrintMembers(StringBuilder builder);
  • Si un tipo de registro no es sealed y deriva de object (es decir, no declara un registro base), la firma se protected virtual bool PrintMembers(StringBuilder builder);
  • Si un tipo de registro no es sealed y deriva de otro registro, la firma es protected override bool PrintMembers(StringBuilder builder);

Estas reglas son más fáciles de comprender mediante la comprensión del propósito de PrintMembers. PrintMembers agrega información sobre cada propiedad de un tipo de registro a una cadena. El contrato requiere que los registros base agreguen sus miembros a la pantalla y da por hecho que los miembros derivados van a agregar sus miembros. Cada tipo de registro sintetiza una invalidación de ToString que es similar al ejemplo siguiente de HeatingDegreeDays:

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

Declare un método PrintMembers en el registro DegreeDays que no imprima el tipo de la colección:

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

La firma declara un método virtual protected para que coincida con la versión del compilador. No se preocupe si obtiene los descriptores de acceso incorrectos; el lenguaje aplica la firma correcta. Si olvida los modificadores correctos para cualquier método sintetizado, el compilador emite advertencias o errores que le ayudan a obtener la firma correcta.

Puede declarar el método ToString como sealed en un tipo de registro. Esto impide que los registros derivados proporcionen una nueva implementación. Los registros derivados seguirán conteniendo la invalidación de PrintMembers. Tendría que sellar ToString si no quisiera que mostrara el tipo de entorno de ejecución del registro. En el ejemplo anterior, perdería la información sobre dónde mide el registro los días con temperaturas altas o bajas.

Mutación no destructiva

Los miembros sintetizados de una clase de registro posicional no modifican el estado del registro. El objetivo es que pueda crear más fácilmente registros inmutables. Recuerde que declara una instancia de readonly record struct para crear una estructura de registro inmutable. Examine de nuevo las declaraciones anteriores para HeatingDegreeDays y CoolingDegreeDays. Los miembros agregados realizan cálculos en los valores del registro, pero no mutan el estado. Los registros posicionales facilitan la creación de tipos de referencia inmutables.

La creación de tipos de referencia inmutables significa que quiere usar una mutación no destructiva. Cree nuevas instancias de registro que sean similares a las instancias de registro existentes mediante expresiones de with. Estas expresiones son una construcción de copia con asignaciones adicionales que modifican la copia. El resultado es una nueva instancia de registro donde cada propiedad se copió del registro existente y, opcionalmente, se modificó. El registro original permanece sin cambios.

Vamos a agregar un par de características al programa que muestren expresiones with. En primer lugar, vamos a crear un nuevo registro para calcular la suma térmica con los mismos datos. La suma térmica suele usar 41 F como base de referencia y mide las temperaturas por encima de ella. Para usar los mismos datos, puede crear un nuevo registro similar al coolingDegreeDays, pero con una temperatura base diferente:

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

Puede comparar el número de grados calculados con los números generados con una temperatura de línea base más alta. Recuerde que los registros son tipos de referencia y estas copias son instantáneas. La matriz de los datos no se copia, pero ambos registros hacen referencia a los mismos datos. Ese hecho es una ventaja en otro escenario. En el caso de la suma térmica, es útil realizar el seguimiento del total de los cinco días anteriores. Puede crear registros con diferentes datos de origen mediante expresiones with. El código siguiente compila una colección de estas acumulaciones y, a continuación, muestra los 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);
}

También puede usar expresiones with para crear copias de registros. No especifique ninguna propiedad entre las llaves de la expresión with. Esto significa crear una copia y no cambiar ninguna propiedad:

var growingDegreeDaysCopy = growingDegreeDays with { };

Ejecute la aplicación finalizada para ver los resultados.

Resumen

En este tutorial se muestran varios aspectos de los registros. Los registros proporcionan una sintaxis concisa para los tipos en los que el uso fundamental almacena datos. Para las clases orientadas a objetos, el uso fundamental es definir las responsabilidades. Este tutorial se centra en registros posicionales, donde puede usar una sintaxis concisa para declarar las propiedades de un registro. El compilador sintetiza varios miembros del registro para copiar y comparar registros. Puede agregar cualquier otro miembro que necesite para sus tipos de registro. Puede crear tipos de registro inmutables sabiendo que ninguno de los miembros generados por el compilador mutaría el estado. Y las expresiones with facilitan la mutación no destructiva.

Los registros agregan otra manera de definir tipos. Las definiciones de class se usan para crear jerarquías orientadas a objetos que se centran en las responsabilidades y el comportamiento de los objetos. Cree tipos struct para las estructuras de datos que almacenan datos y que son lo suficientemente pequeñas como para copiarse de forma eficaz. Puede crear tipos de record cuando desee igualdad y comparación basadas en valores, no desee copiar valores y desee utilizar variables de referencia. Los tipos record struct se crean cuando se quieren las características de los registros para un tipo lo suficientemente pequeño como para copiarlo de forma eficaz.

Puede obtener más información sobre los registros en el artículo de referencia del lenguaje C# de para el tipo de registro y la especificación propuesta de tipo de registro y la especificación de estructura de registro .