Registros (referencia de C#)
A partir de C# 9, se usa la palabra clave record para definir un tipo de referencia que proporciona funcionalidad integrada para encapsular los datos. Puede crear tipos de registros con propiedades inmutables mediante parámetros posicionales o sintaxis de propiedades estándar:
public record Person(string FirstName, string LastName);
public record Person
{
public string FirstName { get; init; } = default!;
public string LastName { get; init; } = default!;
};
También puede crear tipos de registros con propiedades y campos mutables:
public record Person
{
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
};
Aunque los registros pueden ser mutables, están destinados principalmente a admitir modelos de datos inmutables. El tipo de registro ofrece las siguientes características:
- Sintaxis concisa para crear un tipo de referencia con propiedades inmutables
- Comportamiento integrado útil para un tipo de referencia centrado en datos:
- Compatibilidad con las jerarquías de herencia
También puede utilizar tipos de estructura para diseñar tipos centrados en datos que proporcionen igualdad de valores y un comportamiento escaso o inexistente. En C# 10 y versiones posteriores, puede definir tipos record struct mediante parámetros posicionales o sintaxis de propiedades estándar:
public readonly record struct Point(double X, double Y, double Z);
public record struct Point
{
public double X { get; init; }
public double Y { get; init; }
public double Z { get; init; }
}
Las estructuras de registro también pueden ser mutables, tanto estructuras de registro posicionales como estructuras de registro sin parámetros posicionales:
public record struct DataMeasurement(DateTime TakenAt, double Measurement);
public record struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
En los ejemplos anteriores se muestran algunas diferencias entre los registros que son tipos de referencia y que son tipos de valor:
- Un elemento
recordorecord classdeclara un tipo de referencia. La palabra claveclasses opcional, pero puede agregar claridad para los lectores. Un elementorecord structdeclara un tipo de valor. - Las propiedades posicionales son inmutables en
record classyreadonly record struct. Son mutables enrecord struct.
En el resto de este artículo se describen los tipos record class y record struct. Las diferencias se detallan en cada sección. Debe elegir entre record class y record struct, de la misma forma que se elige entre class y struct. El término registro se usa para describir el comportamiento que se aplica a todos los tipos de registro. Se usa record struct o record class para describir el comportamiento que se aplica solo a los tipos de estructura o de clase, respectivamente.
Sintaxis posicional para la definición de propiedad
Puede usar parámetros posicionales para declarar propiedades de un registro e inicializar los valores de propiedad al crear una instancia:
public record Person(string FirstName, string LastName);
public static void Main()
{
Person person = new("Nancy", "Davolio");
Console.WriteLine(person);
// output: Person { FirstName = Nancy, LastName = Davolio }
}
Cuando se usa la sintaxis posicional para la definición de propiedad, el compilador crea lo siguiente:
- Una propiedad pública implementada automáticamente de solo inicialización para cada parámetro posicional proporcionado en la declaración de registro.
- Para los tipos
recordyreadonly record struct: una propiedad de solo inicialización solo se puede establecer en el constructor o mediante un inicializador de propiedad. - Para los tipos
record struct: una propiedad de lectura y escritura que se puede establecer en un constructor, inicializador de propiedad o asignación después de la construcción.
- Para los tipos
- Un constructor primario cuyos parámetros coinciden con los parámetros posicionales en la declaración del registro.
- Para los tipos de estructura de registro, un constructor sin parámetros que establece cada campo en su valor predeterminado.
- Un método
Deconstructcon un parámetrooutpara cada parámetro posicional proporcionado en la declaración de registro. Este método solo se proporciona si hay dos o más parámetros posicionales. El método deconstruye las propiedades definidas mediante la sintaxis posicional; omite las propiedades que se definen mediante la sintaxis de propiedades estándar.
Es posible que le interese agregar atributos a cualquiera de estos elementos que el compilador crea a partir de la definición de registro. Puede agregar un destino a cualquier atributo que aplique a las propiedades del registro posicional. En el ejemplo siguiente se aplica System.Text.Json.Serialization.JsonPropertyNameAttribute a cada propiedad del registro Person. El destino property: indica que el atributo se aplica a la propiedad generada por el compilador. Otros valores son field: para aplicar el atributo al campo y param: para aplicar el atributo al parámetro.
/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")]string FirstName,
[property: JsonPropertyName("lastName")]string LastName);
En el ejemplo anterior también se muestra cómo crear comentarios de documentación XML para el registro. Puede agregar la etiqueta <param> para agregar documentación para los parámetros del constructor principal.
Si la definición de propiedad implementada automáticamente generada no es la que desea, puede definir su propia propiedad con el mismo nombre. Si lo hace, el constructor y el deconstructor generados usarán su definición de propiedad. En el ejemplo siguiente se declaran las propiedades FirstName y LastName de un registro posicional public, pero el parámetro posicional Id se restringe a internal. Puede usar esta sintaxis para registros y tipos de estructura de registros. Debe agregar la asignación explícita de la propiedad declarada a su parámetro posicional correspondiente.
public record Person(string FirstName, string LastName, string Id)
{
internal string Id { get; init; } = Id;
}
public static void Main()
{
Person person = new("Nancy", "Davolio", "12345");
Console.WriteLine(person.FirstName); //output: Nancy
}
Un tipo de registro no tiene que declarar ninguna propiedad posicional. Puede declarar un registro sin propiedades posicionales, y otros campos y propiedades, como en el ejemplo siguiente:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; } = Array.Empty<string>();
};
Si define propiedades mediante la sintaxis de propiedades estándar, pero omite el modificador de acceso, las propiedades son private implícitamente.
Inmutabilidad
Un registro posicional y una estructura de registro de solo lectura posicional declaran propiedades de solo inicialización. Una estructura de registro posicional declara propiedades de lectura y escritura. Puede invalidar cualquiera de esos valores predeterminados, como se ha mostrado en la sección anterior.
La inmutabilidad puede resultar útil si necesita que un tipo centrado en datos sea seguro para subprocesos o si depende de que un código hash quede igual en una tabla hash. Sin embargo, la inmutabilidad no es adecuada para todos los escenarios de datos. Por ejemplo, Entity Framework Core no admite la actualización con tipos de entidad inmutables.
Las propiedades de solo inicialización, tanto si se crean a partir de parámetros posicionales (record class y readonly record struct) como al especificar descriptores de acceso init, tienen una inmutabilidad superficial. Después de la inicialización, no se puede cambiar el valor de las propiedades de tipo de valor ni la referencia de las propiedades de tipo de referencia. Sin embargo, se pueden cambiar los datos a los que hace referencia una propiedad de tipo de referencia. En el ejemplo siguiente se muestra que el contenido de una propiedad inmutable de tipo de referencia (una matriz en este caso) es mutable:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234
person.PhoneNumbers[0] = "555-6789";
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}
Las características exclusivas de los tipos de registro se implementan mediante métodos sintetizados por el compilador, y ninguno de estos métodos pone en peligro la inmutabilidad mediante la modificación del estado del objeto. A menos que se especifique, se generan métodos sintetizados para las declaraciones de record, record struct y readonly record struct.
Igualdad de valores
Para cualquier tipo que defina, puede invalidar Object.Equals(Object) y sobrecargar operator ==. Si no invalida Equals ni sobrecarga operator ==, el tipo que declare controla cómo se define la igualdad:
- Para los tipos
class, dos objetos son iguales si hacen referencia al mismo objeto en memoria. - Para los tipos
struct, dos objetos son iguales si son del mismo tipo y almacenan los mismos valores. - Para los tipos
record, incluidosrecord structyreadonly record struct, dos objetos son iguales si son del mismo tipo y almacenan los mismos valores.
La definición de igualdad para record struct es la misma que para struct. La diferencia es que para struct, la implementación está en ValueType.Equals(Object) y se basa en la reflexión. Para los registros, la implementación se sintetiza en el compilador y usa los miembros de datos declarados.
Se requiere la igualdad de referencia en algunos modelos de datos. Por ejemplo, Entity Framework Core depende de la igualdad de referencia para garantizar que solo usa una instancia de un tipo de entidad para lo que es conceptualmente una entidad. Por esta razón, los registros y las estructuras de registro no son adecuados para su uso como tipos de entidad en Entity Framework Core.
En el ejemplo siguiente se muestra la igualdad de valores de tipos de registro:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
var phoneNumbers = new string[2];
Person person1 = new("Nancy", "Davolio", phoneNumbers);
Person person2 = new("Nancy", "Davolio", phoneNumbers);
Console.WriteLine(person1 == person2); // output: True
person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True
Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}
Para implementar la igualdad de valores, el compilador sintetiza los métodos siguientes:
Una invalidación de Object.Equals(Object).
Este método se utiliza como base para el método estático Object.Equals(Object, Object) cuando ambos parámetros no son NULL.
Un método
Equalsvirtual cuyo parámetro es el tipo de registro. Este método implementa IEquatable<T>.Una invalidación de Object.GetHashCode().
Invalidaciones de los operadores
==y!=.
Puede escribir sus propias implementaciones para reemplazar cualquiera de estos métodos sintetizados. Si un tipo de registro tiene un método que coincide con la signatura de cualquier método sintetizado, el compilador no sintetiza ese método.
Si proporciona su propia implementación de Equals en un tipo de registro, proporcione también una implementación de GetHashCode.
Mutación no destructiva
Si necesita copiar una instancia de registro con algunas modificaciones, puede usar una expresión with para lograr una mutación no destructiva. Una expresión with crea una instancia de registro que es una copia de una instancia de registro existente, con las propiedades y los campos especificados modificados. Use la sintaxis del inicializador de objeto para especificar los valores que se van a cambiar, como se muestra en el ejemplo siguiente:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; }
}
public static void Main()
{
Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
Console.WriteLine(person1);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Person person2 = person1 with { FirstName = "John" };
Console.WriteLine(person2);
// output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { PhoneNumbers = new string[1] };
Console.WriteLine(person2);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { };
Console.WriteLine(person1 == person2); // output: True
}
La expresión with puede establecer propiedades posicionales o propiedades creadas con la sintaxis de propiedades estándar. Las propiedades no posicionales deben tener un descriptor de acceso init o set para cambiar en una expresión with.
El resultado de una expresión with es una copia superficial, lo que significa que, para una propiedad de referencia, solo se copia la referencia a una instancia. Tanto el registro original como la copia terminan con una referencia a la misma instancia.
A fin de implementar esta característica para los tipos record class, el compilador sintetiza un método de clonación y un constructor de copia. El método de clonación virtual devuelve un nuevo registro inicializado por el constructor de copia. Cuando se usa una expresión with, el compilador crea código que llama al método de clonación y, después, establece las propiedades que se especifican en la expresión with.
Si necesita otro comportamiento de copia, puede escribir un constructor de copia propio en una instancia de record class. Si lo hace, el compilador no sintetizará un método. Cree su constructor private si el registro es sealed; de lo contrario, conviértalo en protected. El compilador no sintetiza un constructor de copia para los tipos record struct. Puede escribir uno, pero el compilador no generará llamadas a él para las expresiones with. En su lugar, el compilador usa la asignación.
No puede invalidar el método de clonación y no puede crear un miembro denominado Clone en ningún tipo de registro. El nombre real del método de clon lo genera el compilador.
Formato integrado para la presentación
Los tipos de registros tienen un método ToString generado por el compilador que muestra los nombres y los valores de las propiedades y los campos públicos. El método ToString devuelve una cadena con el formato siguiente:
<record type name> { <property name> = <value>, <property name> = <value>, ...}
En el caso de los tipos de referencia, se muestra el nombre del tipo del objeto al que hace referencia la propiedad en lugar del valor de propiedad. En el ejemplo siguiente, la matriz es un tipo de referencia, por lo que se muestra System.String[] en lugar de los valores de los elementos de matriz reales:
Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }
Para implementar esta característica, en los tipos record class el compilador sintetiza un método PrintMembers virtual y una invalidación de ToString. En los tipos record struct, este miembro es private.
La invalidación ToString crea un objeto StringBuilder con el nombre de tipo seguido de un corchete de apertura. Llama a PrintMembers para agregar nombres y valores de propiedad y, a continuación, agrega el corchete de cierre. En el ejemplo siguiente se muestra código similar al que contiene la invalidación sintetizada:
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Teacher"); // type name
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}
Puede proporcionar su propia implementación de PrintMembers o la invalidación ToString. En la sección Formato PrintMembers en registros derivados que se encuentra más adelante en este artículo se proporcionan ejemplos. En C# 10 y versiones posteriores, la implementación de ToString puede incluir el modificador sealed, lo que impide que el compilador sintetice una implementación de ToString para los registros derivados. De hecho, esto significa que la salida de ToString no incluirá la información de tipos en tiempo de ejecución. (Se muestran todos los miembros y valores, porque los registros derivados seguirán generando un método PrintMembers).
Herencia
Esta sección solo se aplica a los tipos record class.
Un registro puede heredar de otro registro. Sin embargo, un registro no puede heredar de una clase, y una clase no puede heredar de un registro.
Parámetros posicionales en tipos de registro derivados
El registro derivado declara parámetros para todos los parámetros del constructor primario del registro base. El registro base declara e inicializa esas propiedades. El registro derivado no las oculta, sino que solo crea e inicializa propiedades para los parámetros que no se han declarado en su registro base.
En el ejemplo siguiente se muestra la herencia con la sintaxis de la propiedad posicional:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
Igualdad en las jerarquías de herencia
Esta sección se aplica a los tipos record class, pero no a los tipos record struct. Para que dos variables de registro sean iguales, el tipo en tiempo de ejecución debe ser el mismo. Los tipos de las variables contenedoras podrían ser diferentes. La comparación de igualdad heredada se muestra en el ejemplo de código siguiente:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Person student = new Student("Nancy", "Davolio", 3);
Console.WriteLine(teacher == student); // output: False
Student student2 = new Student("Nancy", "Davolio", 3);
Console.WriteLine(student2 == student); // output: True
}
En el ejemplo, todas las variables se declaran como Person, incluso cuando la instancia es un tipo derivado de Student o Teacher. Las instancias tienen las mismas propiedades y los mismos valores de propiedad. Pero student == teacher devuelve False, aunque ambas son variables de tipo Person, y student == student2 devuelve True, aunque una es una variable Person y otra es una variable Student. La prueba de igualdad depende del tipo en tiempo de ejecución del objeto real, no del tipo declarado de la variable.
Para implementar este comportamiento, el compilador sintetiza una propiedad EqualityContract que devuelve un objeto Type que coincide con el tipo del registro. EqualityContract permite a los métodos de igualdad comparar el tipo en tiempo de ejecución de los objetos cuando comprueban la igualdad. Si el tipo base de un registro es object, esta propiedad es virtual. Si el tipo base es otro tipo de registro, la propiedad es una invalidación. Si el tipo de registro es sealed, esta propiedad es sealed.
Al comparar dos instancias de un tipo derivado, los métodos de igualdad sintetizados comprueban la igualdad de todas las propiedades de los tipos base y derivados. El método GetHashCode sintetizado usa el método GetHashCode de todas las propiedades y los campos declarados en el tipo base y el tipo de registro derivado.
Expresiones with en registros derivados
El resultado de una expresión with tiene el mismo tipo de entorno de ejecución que el operando de la expresión. Se copian todas las propiedades del tipo en tiempo de ejecución, pero solo se pueden establecer las propiedades del tipo en tiempo de compilación, como se muestra en el ejemplo siguiente:
public record Point(int X, int Y)
{
public int Zbase { get; set; }
};
public record NamedPoint(string Name, int X, int Y) : Point(X, Y)
{
public int Zderived { get; set; }
};
public static void Main()
{
Point p1 = new NamedPoint("A", 1, 2) { Zbase = 3, Zderived = 4 };
Point p2 = p1 with { X = 5, Y = 6, Zbase = 7 }; // Can't set Name or Zderived
Console.WriteLine(p2 is NamedPoint); // output: True
Console.WriteLine(p2);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = A, Zderived = 4 }
Point p3 = (NamedPoint)p1 with { Name = "B", X = 5, Y = 6, Zbase = 7, Zderived = 8 };
Console.WriteLine(p3);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = B, Zderived = 8 }
}
Formato PrintMembers en registros derivados
El método sintetizado PrintMembers de un tipo de registro derivado llama a la implementación base. El resultado es que todas las propiedades y los campos públicos de los tipos derivados y base se incluyen en la salida ToString, como se muestra en el ejemplo siguiente:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
Puede proporcionar su propia implementación del método PrintMembers. Si lo hace, use la siguiente firma:
- Para un registro
sealedque deriva deobject(no declara un registro base):private bool PrintMembers(StringBuilder builder). - Para un registro
sealedque deriva de otro registro:protected sealed override bool PrintMembers(StringBuilder builder). - Para un registro que no es
sealedy que deriva del objeto:protected virtual bool PrintMembers(StringBuilder builder);. - Para un registro que no es
sealedy que deriva de otro registro:protected override bool PrintMembers(StringBuilder builder);.
Este es un ejemplo de código que reemplaza los métodos sintetizados PrintMembers, uno para un tipo de registro que se deriva de un objeto y otro para un tipo de registro que se deriva de otro registro:
public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
return true;
}
}
public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
: Person(FirstName, LastName, PhoneNumbers)
{
protected override bool PrintMembers(StringBuilder stringBuilder)
{
if (base.PrintMembers(stringBuilder))
{
stringBuilder.Append(", ");
};
stringBuilder.Append($"Grade = {Grade}");
return true;
}
};
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", new string[2] { "555-1234", "555-6789" }, 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, PhoneNumber1 = 555-1234, PhoneNumber2 = 555-6789, Grade = 3 }
}
Nota
En C# 10 y versiones posteriores, el compilador sintetizará PrintMembers cuando un registro base haya sellado el método ToString. También puede crear una implementación de PrintMembers propia.
Comportamiento del deconstructor en registros derivados
El método Deconstruct de un registro derivado devuelve los valores de todas las propiedades posicionales del tipo en tiempo de compilación. Si el tipo de variable es un registro base, solo se deconstruyen las propiedades del registro base a menos que el objeto se convierta en el tipo derivado. En el ejemplo siguiente se muestra cómo llamar a un deconstructor en un registro derivado.
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
var (firstName, lastName) = teacher; // Doesn't deconstruct Grade
Console.WriteLine($"{firstName}, {lastName}");// output: Nancy, Davolio
var (fName, lName, grade) = (Teacher)teacher;
Console.WriteLine($"{fName}, {lName}, {grade}");// output: Nancy, Davolio, 3
}
Restricciones genéricas
No hay ninguna restricción genérica en la que sea necesario que un tipo sea un registro. Los registros satisfacen la restricción class o struct. Para realizar una restricción en una jerarquía específica de tipos de registro, coloque la restricción en el registro base como lo haría con una clase base. Para obtener más información, vea Restricciones de tipos de parámetros.
especificación del lenguaje C#
Para más información, vea la sección Clases de la especificación del lenguaje C#.
Para obtener más información sobre de las características presentadas en C# 9 y versiones posteriores, vea las siguientes notas de propuesta de características: