Registros (referencia de C#)

Use el modificador record para definir un tipo de referencia que proporciona funcionalidad integrada para encapsular datos. C# 10 permite la sintaxis record class como sinónimo para aclarar un tipo de referencia y record struct para definir un tipo de valor con una funcionalidad similar.

Cuando se declara un constructor principal en un registro, el compilador genera propiedades públicas de los parámetros del constructor principal. Los parámetros de constructor principal de un registro se conocen como parámetros posicionales. El compilador crea propiedades posicionales que reflejan el constructor principal o los parámetros posicionales. El compilador no sintetiza las propiedades de los parámetros de constructor principal en tipos que no tengan el modificador record.

En los dos ejemplos siguientes se muestran tipos de referencia record (o record class):

public record Person(string FirstName, string LastName);
public record Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
};

En los dos ejemplos siguientes se muestran tipos de valor record struct:

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

También puede crear registros con propiedades y campos mutables:

public record Person
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
};

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

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:

En los ejemplos anteriores se muestran algunas diferencias entre los registros que son tipos de referencia y que son tipos de valor:

  • Un elemento record o record class declara un tipo de referencia. La palabra clave class es opcional, pero puede agregar claridad para los lectores. Un elemento record struct declara un tipo de valor.
  • Las propiedades posicionales son inmutables en record class y readonly record struct. Son mutables en record 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. El tipo record struct se introdujo en C# 10.

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 por cada parámetro posicional proporcionado en la declaración del registro.
    • Para los tipos record y readonly record struct: una propiedad init-only.
    • Para los tipos record struct: una propiedad read-write.
  • 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 Deconstruct con un parámetro out para cada parámetro posicional proporcionado en la declaración de registro. 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. Por ejemplo, puede cambiar la accesibilidad o la mutabilidad, o proporcionar una implementación para el descriptor de acceso get o set. Si declara la propiedad en el origen, debe inicializarla desde el parámetro posicional del registro. Si la propiedad es una propiedad implementada automáticamente, debe inicializarla. Si agrega un campo de respaldo en el origen, debe inicializarlo. El deconstructor generado usa la 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.

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; } = [];
};

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

Si no invalida o reemplaza métodos de igualdad, el tipo que declara rige 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.
  • En los tipos con el modificador record (record class, record struct y readonly 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). Se trata de un error si la invalidación se declara explícitamente.

    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 virtual, o sealed, Equals(R? other) donde R es el tipo de registro. Este método implementa IEquatable<T>. Este método se puede declarar explícitamente.

  • Si el tipo de registro se deriva de un tipo de registro base Base, Equals(Base? other). Se trata de un error si la invalidación se declara explícitamente. Si proporciona su propia implementación de Equals(R? other) en un tipo de registro, proporcione también una implementación de GetHashCode.

  • Una invalidación de Object.GetHashCode(). Este método se puede declarar explícitamente.

  • Invalidaciones de los operadores == y !=. Se trata de un error si los operadores se declaran explícitamente.

  • Si el tipo de registro se deriva de un tipo de registro base, protected override Type EqualityContract { get; };. Esta propiedad se puede declarar explícitamente. Para obtener más información, consulte Igualdad en jerarquías de herencia.

El compilador no sintetiza un método cuando un tipo de registro tiene un método que coincide con la firma de un método sintetizado que se puede declarar explícitamente.

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 declaradas explícitamente 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 sintetiza 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 genera llamadas a él para las expresiones with. Los valores de record struct se copian en 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:

<nombre del tipo de registro> {<nombre de la propiedad> = <valor>, <nombre de la propiedad> = <valor>, ...}

La cadena impresa para <value> es la cadena devuelta por ToString() para el tipo de la propiedad. En el ejemplo siguiente, ChildNames es System.Array, donde ToString devuelve System.String[]:

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. Puede crear una representación de cadena coherente en una jerarquía de tipos record (los registros derivados siguen teniendo un método PrintMembers generado para todas las propiedades derivadas).

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 tiene un estado sealed eficaz porque el tipo es sealed.

Cuando el código compara dos instancias de un tipo derivado, los métodos de igualdad sintetizados comprueban todos los miembros de datos de las propiedades de los tipos base y derivados. El método GetHashCode sintetizado usa el método GetHashCode de todos los miembros de datos declarados en el tipo base y el tipo de registro derivado. Los miembros de datos de record incluyen todos los campos declarados y el campo de respaldo sintetizado por el compilador de cualquier propiedad implementada automáticamente.

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 sealed que deriva de object (no declara un registro base): private bool PrintMembers(StringBuilder builder).
  • Para un registro sealed que se deriva de otro registro (tenga en cuenta que el tipo envolvente es sealed, por lo que el método tiene un estado sealed eficaz): protected override bool PrintMembers(StringBuilder builder).
  • Para un registro que no es sealed y que deriva del objeto: protected virtual bool PrintMembers(StringBuilder builder);.
  • Para un registro que no es sealed y 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 en registros derivados 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

La palabra clave record es un modificador de un tipo class o struct. Al agregar el modificador record, se contempla el comportamiento descrito anteriormente en este artículo. No hay ninguna restricción genérica en la que sea necesario que un tipo sea un registro. Un objeto record class cumple la restricción class. Un objeto record struct cumple la restricción struct. 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 estas características, consulte las siguientes notas de propuesta de características:

Vea también