Compartir a través de


Structs de registro

Nota

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos e se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones de .

La sintaxis de una estructura de registro es la siguiente:

record_struct_declaration
    : attributes? struct_modifier* 'partial'? 'record' 'struct' identifier type_parameter_list?
      parameter_list? struct_interfaces? type_parameter_constraints_clause* record_struct_body
    ;

record_struct_body
    : struct_body
    | ';'
    ;

Los tipos de estructura de registro son tipos de valor, como otros tipos de estructura. Heredan implícitamente de la clase System.ValueType. Los modificadores y miembros de una estructura de registro están sujetos a las mismas restricciones que los de las estructuras (accesibilidad en el tipo, modificadores en miembros, inicializadores de instancias para constructores base(...), asignación definitiva para this en el constructor, destructores, ...). Las estructuras de registro también seguirán las mismas reglas que las estructuras para los constructores de instancias sin parámetros e inicializadores de campo, pero en este documento se supone que se eliminarán esas restricciones para las estructuras en general.

Consulte §16.4.9 Consulte la especificación de constructores de struct sin parámetros.

Las estructuras de registro no pueden usar el modificador ref.

Como máximo, una declaración de tipo parcial de una estructura de registro parcial puede proporcionar un parameter_list. El parameter_list puede estar vacío.

Los parámetros de estructura de registro no pueden usar modificadores ref, out o this (pero se permiten in y params).

Miembros de un struct de registro

Además de los miembros declarados en el cuerpo de la estructura de registro, un tipo de estructura de registro tiene miembros sintetizados adicionales. Los miembros se sintetizan a menos que un miembro con una firma "igual" se declare en el cuerpo del struct de registro o se herede un miembro concreto no virtual accesible con una firma "igual". Dos miembros se consideran iguales si tienen la misma firma o se consideran "ocultos" en condición de heredado. Consulte Firmas y sobrecarga §7.6. Es un error que un miembro de un registro se llame "Clone" (clon).

Es un error que un campo de instancia de una estructura de registro tenga un tipo inseguro.

No se permite que un struct de registro declare un destructor.

Los miembros sintetizados son los siguientes:

Miembros por la igualdad

Los miembros en igualdad sintetizados son similares a los de una clase de registro (Equals para este tipo, Equals para el tipo object, == y operadores != para este tipo),
excepto que no hay EqualityContract, comprobaciones de valores NULL ni herencias.

El struct de registro implementa System.IEquatable<R> e incluye una sobrecarga sintetizada con un tipado fuerte de Equals(R other), donde R es el struct de registro. El método es public. El método se puede declarar explícitamente. Se trata de un error si la declaración explícita no coincide con la firma o accesibilidad esperadas.

Si Equals(R other) está definido por el usuario (no sintetizado), pero GetHashCode no es así, se genera una advertencia.

public readonly bool Equals(R other);

El Equals(R) sintetizado devuelve true siempre y cuando en cada campo de instancia fieldN en el struct de registro el valor de System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN), donde TN es el tipo de campo, es true.

El struct de registro incluye los operadores sinterizados == y != equivalentes a los operadores declarados de la siguiente manera:

public static bool operator==(R r1, R r2)
    => r1.Equals(r2);
public static bool operator!=(R r1, R r2)
    => !(r1 == r2);

El método Equals llamado por el operador == es el método Equals(R other) especificado anteriormente. El operador != delega al operador ==. Es un error si los operadores se declaran explícitamente.

La estructura de registro incluye una sobrescritura sintetizada equivalente a un método declarado como se muestra a continuación:

public override readonly bool Equals(object? obj);

Se trata de un error si la invalidación se declara explícitamente. La invalidación sintetizada devuelve other is R temp && Equals(temp), donde R es el struct de registro.

La estructura de registro incluye una sobrescritura sintetizada que es equivalente a un método declarado de la siguiente manera:

public override readonly int GetHashCode();

El método se puede declarar explícitamente.

Se genera una advertencia si uno de Equals(R) y GetHashCode() se declara explícitamente, pero el otro método no es explícito.

La invalidación sintetizada de GetHashCode() devuelve un int, resultado de combinar los valores de System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) en cada campo de la instancia fieldN, donde TN es el tipo de fieldN.

Por ejemplo, considere la siguiente estructura de registro:

record struct R1(T1 P1, T2 P2);

Para este struct de registro, los miembros en igualdad sintetizados serían algo parecido a lo siguiente:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }
    public override bool Equals(object? obj) => obj is R1 temp && Equals(temp);
    public bool Equals(R1 other)
    {
        return
            EqualityComparer<T1>.Default.Equals(P1, other.P1) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R1 r1, R1 r2)
        => r1.Equals(r2);
    public static bool operator!=(R1 r1, R1 r2)
        => !(r1 == r2);    
    public override int GetHashCode()
    {
        return Combine(
            EqualityComparer<T1>.Default.GetHashCode(P1),
            EqualityComparer<T2>.Default.GetHashCode(P2));
    }
}

Miembros de impresión: métodos PrintMembers y ToString

La estructura de registro incluye un método sintetizado equivalente a un método declarado de la siguiente manera:

private bool PrintMembers(System.Text.StringBuilder builder);

El método hace lo siguiente:

  1. para cada uno de los miembros imprimibles del struct de registro (miembros de campo público no estáticos y miembros de propiedad legibles), anexa el nombre del miembro seguido de "=", seguido del valor del miembro, separado por ",";
  2. devuelve el valor true si el struct de registro tiene miembros imprimibles.

Para un miembro que tiene un tipo de valor, convertiremos su valor en una representación de cadena mediante el método más eficaz disponible para la plataforma de destino. En estos momentos, esto implica llamar a ToString antes de pasar a StringBuilder.Append.

Si los miembros imprimibles del registro no incluyen una propiedad legible con un descriptor de acceso que no sea de readonlyget, el PrintMembers sintetizado será readonly. No se requiere que los campos del registro sean readonly para que el método PrintMembers sea readonly.

El método PrintMembers se puede declarar explícitamente. Se trata de un error si la declaración explícita no coincide con la firma o accesibilidad esperadas.

La estructura de registro incluye un método sintetizado equivalente a un método declarado de la siguiente manera:

public override string ToString();

Si el método PrintMembers de la estructura de registro es readonly, entonces el método ToString() sintetizado es readonly.

El método se puede declarar explícitamente. Se trata de un error si la declaración explícita no coincide con la firma o accesibilidad esperadas.

El método sintetizado:

  1. crea una instancia de StringBuilder,
  2. anexa el nombre del struct de registro al constructor, seguido de "{",
  3. invoca el método PrintMembers del struct de registro con el constructor, seguido de " " si ha devuelto el valor true,
  4. anexa "}",
  5. devuelve los contenidos del constructor con builder.ToString().

Por ejemplo, considere la siguiente estructura de registro:

record struct R1(T1 P1, T2 P2);

Para este struct de registro, los miembros de impresión sintetizados serían algo parecido a lo siguiente:

struct R1 : IEquatable<R1>
{
    public T1 P1 { get; set; }
    public T2 P2 { get; set; }

    private bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if P1 has a value type
        builder.Append(", ");

        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2.ToString()); if P2 has a value type

        return true;
    }

    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R1));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

Miembros de struct de registro posicional

Además de los miembros mencionados anteriormente, los struct de registro que poseen una lista de parámetros ("registros posicionales") generan miembros adicionales según las mismas condiciones que los ya indicados.

Constructor principal

Una estructura de registro tiene un constructor público cuya firma corresponde a los parámetros de valor de la declaración de tipo. Esto se denomina constructor principal del tipo. Es un error tener un constructor principal y un constructor con la misma signatura ya presente en la structura. Si la declaración de tipo no incluye una lista de parámetros, no se genera ningún constructor principal.

record struct R1
{
    public R1() { } // ok
}

record struct R2()
{
    public R2() { } // error: 'R2' already defines constructor with same parameter types
}

Las declaraciones de campo de instancia para una estructura de registro pueden incluir inicializadores de variables. Si no hay ningún constructor principal, los inicializadores de instancia se ejecutan como parte del constructor sin parámetros. De lo contrario, en tiempo de ejecución, el constructor principal ejecuta los inicializadores de instancia que aparecen en el "record-struct-body".

Si un struct de registro tiene un constructor principal, cualquier constructor definido por el usuario debe tener un inicializador de constructor this explícito que llame al constructor principal o a un constructor declarado explícitamente.

Los parámetros del constructor principal, así como los miembros del struct de registro, están disponibles en los inicializadores de campos o propiedades de instancia. Los miembros de instancia serían un error en estas ubicaciones (de igual forma que los miembros de instancia se aplican en los inicializadores de constructores normales hoy en día, pero es un error usarlos), pero los parámetros del constructor principal sí se aplicarían, se podrían utilizar y eclipsarían a los miembros. Los miembros estáticos también serían utilizables.

Se genera una advertencia si no se lee un parámetro del constructor principal.

Las reglas de asignación definitivas para constructores de instancia de struct se aplican al constructor principal de struct de registro. Por ejemplo, lo siguiente es un error:

record struct Pos(int X) // definite assignment error in primary constructor
{
    private int x;
    public int X { get { return x; } set { x = value; } } = X;
}

Propiedades

En cada parámetro de una declaración de struct de registro, hay un miembro de propiedad pública correspondiente cuyo nombre y tipo se toman de la declaración del parámetro de valor.

En un struct de registro:

  • Se crea una propiedad automática pública get y init si el struct de registro tiene el modificador readonly; si no, sería get y set. Ambos tipos de grupos de descriptores de acceso (set y init) se consideran "compatibles". Por lo tanto, el usuario puede declarar una propiedad de solo inicialización en lugar de una mutable sintetizada. Una propiedad abstract heredada con el mismo tipo se invalida. No se crea ninguna propiedad automática si la estructura de registro tiene un campo de instancia con el nombre y el tipo esperados. Es un error si la propiedad heredada no tiene accesores publicget y set/init. Se trata de un error si la propiedad o el campo heredados están ocultos.
    La propiedad automática se inicializa en el valor del parámetro de constructor principal correspondiente. Los atributos se pueden aplicar a la propiedad automática sintetizada y su campo auxiliar usando los destinos property: o field: para los atributos sintácticamente aplicados al parámetro del struct de registro correspondiente.

Deconstruct

Un struct de registro posicional con al menos un parámetro genera un método de instancia público que devuelve un valor nulo denominado Deconstruct, con una declaración de parámetro "out" en cada parámetro de la declaración del constructor principal. Cada parámetro del método Deconstruct tiene el mismo tipo que el parámetro correspondiente de la declaración del constructor principal. El cuerpo del método asigna a cada parámetro del método Deconstruct el valor obtenido del acceso de un miembro de instancia que tiene el mismo nombre. Si los miembros de instancia a los que se accede en el cuerpo no incluyen una propiedad con un descriptor de acceso que no es de readonlyget, el método Deconstruct sintetizado será readonly. El método se puede declarar explícitamente. Se trata de un error si la declaración explícita no coincide con la firma o accesibilidad esperadas, o si es estática.

Permitir la expresión with en estructuras

Ahora es válido que un receptor en una expresión with tenga un tipo de estructura.

A la derecha de la expresión with hay un member_initializer_list con una secuencia de asignaciones del identificador, que debe ser un campo de instancia accesible o una propiedad del tipo del receptor.

En un receptor con tipo de struct, primero se copia el objeto receptor y luego cada member_initializer se procesa de manera similar a una asignación a un campo o a un acceso de propiedad del resultado tras la conversión. Las asignaciones se procesan en orden léxico.

Mejoras en los registros

Permitir record class

La sintaxis existente para los tipos de registro permite record class con el mismo significado que record:

record_declaration
    : attributes? class_modifier* 'partial'? 'record' 'class'? identifier type_parameter_list?
      parameter_list? record_base? type_parameter_constraints_clause* record_body
    ;

Permitir que los miembros posicionales definidos por el usuario sean campos

Consulte https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-10-05.md#changing-the-member-type-of-a-primary-constructor-parameter

No se crea ninguna propiedad automática si el registro tiene o hereda un campo de instancia con el nombre y el tipo esperados.

Permitir constructores sin parámetros e inicializadores de miembros en estructuras

Consulte la especificación de constructores de struct sin parámetros.

Preguntas abiertas

  • ¿cómo reconocer estructuras de registro en metadatos? (no tenemos un método de clonación indescriptible para aprovechar...)

Respondido

  • confirme que queremos mantener el diseño de PrintMembers (método independiente que devuelve bool) (respuesta: sí)
  • confirme que no permitiremos record ref struct (problema con IEquatable<RefStruct> y campos ref) (respuesta: sí)
  • confirmar la implementación de elementos en igualdad. La opción alternativa consiste en que bool Equals(R other), bool Equals(object? other) y los operadores sintetizados deleguen en ValueType.Equals. (respuesta: sí)
  • confirme que queremos permitir inicializadores de campo cuando haya un constructor principal. ¿También queremos permitir constructores de struct sin parámetros ya que estamos en ello (aparentemente se ha corregido el problema de Activator)? (respuesta: Sí, se debe revisar la especificación actualizada en LDM)
  • ¿Cuánto queremos decir sobre el método Combine? (respuesta: lo menos posible)
  • ¿se debería no permitir un constructor definido por el usuario con una firma de constructor de copia? (respuesta: no, no hay ninguna noción de constructor de copia en la especificación de las estructuras de registro)
  • confirmar que no autorizamos a los miembros denominados "Clone". (respuesta: correcto)
  • compruebe que la lógica de Equals sintetizada es funcionalmente equivalente a la implementación en tiempo de ejecución (por ejemplo, float). NaN) (respuesta: confirmada en LDM)
  • ¿Se pueden colocar atributos que tienen como objetivo el campo o la propiedad en la lista de parámetros posicionales? (respuesta: sí, igual que para la clase de registro)
  • ¿with en genéricos? (respuesta: fuera del ámbito de C# 10)
  • debe GetHashCode incluir un hash del propio tipo para obtener valores diferentes entre record struct S1; y record struct S2;? (respuesta: no)