Archivo
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 .
Esta propuesta realiza un seguimiento de la especificación de la característica de registros de C# 9, según lo acordado por el equipo de diseño del lenguaje C#.
La sintaxis de un registro es la siguiente:
record_declaration
: attributes? class_modifier* 'partial'? 'record' identifier type_parameter_list?
parameter_list? record_base? type_parameter_constraints_clause* record_body
;
record_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? ',' interface_type_list
;
record_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
Los tipos de registro son tipos de referencia, similares a una declaración de clase. Es un error que un registro proporcione un record_base
argument_list
si el record_declaration
no contiene un parameter_list
.
Como máximo, una declaración de tipo parcial de un registro parcial puede proporcionar un parameter_list
.
Los parámetros de registro no pueden usar modificadores ref
, out
o this
(pero se permiten in
y params
).
Herencia
Los registros no pueden heredar de clases a menos que la clase sea object
, y las clases no pueden heredar de los registros. Los registros pueden heredar de otros registros.
Miembros de un tipo de registro
Además de los miembros declarados en el cuerpo del registro, un tipo 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 registro o se herede un miembro concreto no virtual accesible con una firma "igual". Un miembro coincidente impide que el compilador genere ese miembro, no ningún otro miembro sintetizado. Dos miembros se consideran iguales si tienen la misma firma o se consideran "ocultos" en condición de heredado. Es un error que un miembro de un registro se llame "Clone". Es un error que un campo de instancia de un registro tenga un tipo no seguro.
Los miembros sintetizados son los siguientes:
Miembros por la igualdad
Si el registro se deriva de object
, el tipo de registro incluye una propiedad de solo lectura sintetizada equivalente a una propiedad declarada de la siguiente manera:
Type EqualityContract { get; }
Si el tipo de registro es private
, la propiedad es sealed
. De lo contrario, la propiedad es virtual
y protected
.
La propiedad 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 la declaración explícita no permite reemplazarla en un tipo derivado y el tipo de registro no es sealed
.
Si el tipo de registro se deriva de un tipo de registro base Base
, el tipo de registro incluye una propiedad de solo lectura sintetizada equivalente a una propiedad declarada de la siguiente manera:
protected override Type EqualityContract { get; }
La propiedad 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 la declaración explícita no permite reemplazarla en un tipo derivado y el tipo de registro no es sealed
. Es un error que la propiedad sintetizada o declarada explícitamente no sobrescriba una propiedad con esta firma en el tipo de registro Base
(por ejemplo, si la propiedad falta en la Base
, está sellada o no es virtual, etc.).
La propiedad sintetizada devuelve typeof(R)
donde R
es el tipo de registro.
El tipo de registro implementa System.IEquatable<R>
e incluye una sobrecarga sintetizada de tipo fuerte de Equals(R? other)
, donde R
es el tipo de registro.
El método es public
, y el método es virtual
a menos que el tipo de registro sea sealed
.
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 la declaración explícita no permite reemplazarla en un tipo derivado y el tipo de registro no es sealed
.
Si Equals(R? other)
está definido por el usuario (no sintetizado), pero GetHashCode
no es así, se genera una advertencia.
public virtual bool Equals(R? other);
El Equals(R?)
sintetizado devuelve true
si y solo si cada uno de los siguientes son true
:
other
no esnull
, y- Para cada campo de instancia
fieldN
en el tipo de registro que no se hereda, el valor deSystem.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)
dondeTN
es el tipo de campo y - Si hay un tipo de registro base, el valor de
base.Equals(other)
(una llamada no virtual apublic virtual bool Equals(Base? other)
); de lo contrario, el valor deEqualityContract == other.EqualityContract
.
El tipo de registro incluye operadores sintetizados ==
y !=
que son equivalentes a operadores declarados de la siguiente manera:
public static bool operator==(R? left, R? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R? left, R? right)
=> !(left == right);
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.
Si el tipo de registro se deriva de un tipo de registro base Base
, el tipo de registro incluye una invalidación sintetizada equivalente a un método declarado de la siguiente manera:
public sealed override bool Equals(Base? other);
Se trata de un error si la invalidación se declara explícitamente. Se trata de un error si el método no invalida un método con la misma firma en el tipo de registro Base
(por ejemplo, si falta el método en el Base
o sellado, o no virtual, etc.).
La invalidación sintetizada devuelve Equals((object?)other)
.
El tipo de registro incluye una sobrescritura sintetizada equivalente a un método declarado de la siguiente manera:
public override bool Equals(object? obj);
Se trata de un error si la invalidación se declara explícitamente. Es un error que el método no invalide object.Equals(object? obj)
(por ejemplo, debido al sombreado en tipos base intermedios, etc.).
La invalidación sintetizada devuelve Equals(other as R)
, donde R
es el tipo de registro.
El tipo de registro incluye una anulación sintetizada equivalente a un método declarado de la siguiente manera:
public override int GetHashCode();
El método se puede declarar explícitamente.
Se trata de un error si la declaración explícita no permite reemplazarla en un tipo derivado y el tipo de registro no es sealed
. Es un error que un método sintetizado o declarado explícitamente no sobreescriba object.GetHashCode()
(por ejemplo, debido a la ocultación en tipos base intermedios, etc.).
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 resultado int
al combinar los siguientes valores:
- Para cada campo de instancia
fieldN
en el tipo de registro que no se hereda, el valor deSystem.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)
dondeTN
es el tipo de campo y - Si hay un tipo de registro base, el valor de
base.GetHashCode()
. De lo contrario, el valor deSystem.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract)
.
Por ejemplo, considere los siguientes tipos de registro:
record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);
Para esos tipos de registro, los miembros de igualdad sintetizados podrían ser similares a:
class R1 : IEquatable<R1>
{
public T1 P1 { get; init; }
protected virtual Type EqualityContract => typeof(R1);
public override bool Equals(object? obj) => Equals(obj as R1);
public virtual bool Equals(R1? other)
{
return !(other is null) &&
EqualityContract == other.EqualityContract &&
EqualityComparer<T1>.Default.Equals(P1, other.P1);
}
public static bool operator==(R1? left, R1? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R1? left, R1? right)
=> !(left == right);
public override int GetHashCode()
{
return HashCode.Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),
EqualityComparer<T1>.Default.GetHashCode(P1));
}
}
class R2 : R1, IEquatable<R2>
{
public T2 P2 { get; init; }
protected override Type EqualityContract => typeof(R2);
public override bool Equals(object? obj) => Equals(obj as R2);
public sealed override bool Equals(R1? other) => Equals((object?)other);
public virtual bool Equals(R2? other)
{
return base.Equals((R1?)other) &&
EqualityComparer<T2>.Default.Equals(P2, other.P2);
}
public static bool operator==(R2? left, R2? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R2? left, R2? right)
=> !(left == right);
public override int GetHashCode()
{
return HashCode.Combine(base.GetHashCode(),
EqualityComparer<T2>.Default.GetHashCode(P2));
}
}
class R3 : R2, IEquatable<R3>
{
public T3 P3 { get; init; }
protected override Type EqualityContract => typeof(R3);
public override bool Equals(object? obj) => Equals(obj as R3);
public sealed override bool Equals(R2? other) => Equals((object?)other);
public virtual bool Equals(R3? other)
{
return base.Equals((R2?)other) &&
EqualityComparer<T3>.Default.Equals(P3, other.P3);
}
public static bool operator==(R3? left, R3? right)
=> (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R3? left, R3? right)
=> !(left == right);
public override int GetHashCode()
{
return HashCode.Combine(base.GetHashCode(),
EqualityComparer<T3>.Default.GetHashCode(P3));
}
}
Copiar y clonar miembros
Un tipo de registro contiene dos miembros de copia:
- Constructor que acepta un único parámetro del tipo de registro. Se conoce como "constructor de copia".
- Método "clone" de instancia pública sin parámetros sintetizada con un nombre reservado del compilador
El propósito del constructor de copia es copiar el estado del parámetro a la nueva instancia que se va a crear. Este constructor no ejecuta ningún inicializador de campo o propiedad de instancia presente en la declaración de registro. Si el compilador no declara explícitamente el constructor, el compilador sintetizará un constructor. Si el registro está sellado, el constructor será privado; de lo contrario, será protegido. Un constructor de copia declarado explícitamente debe ser público o protegido, a menos que el registro esté sellado. Lo primero que debe hacer el constructor es llamar a un constructor de copia de la base o a un constructor de objetos sin parámetros si el registro hereda del objeto . Se notifica un error si un constructor de copia definido por el usuario usa un inicializador de constructor implícito o explícito que no cumple este requisito. Una vez invocado un constructor de copia base, un constructor de copia sintetizado copia los valores de todos los campos de instancia implícita o explícitamente declarados dentro del tipo de registro. La única presencia de un constructor de copia, ya sea explícita o implícita, no impide una adición automática de un constructor de instancia predeterminado.
Si un método virtual "clone" está presente en el registro base, el método "clone" generado lo sobrescribe y el tipo de valor devuelto del método es el tipo que lo contiene actualmente. Se produce un error si el método de clonación del registro base está sellado. Si un método virtual "clone" no está presente en el registro base, el tipo de valor devuelto del método clone es el tipo contenedor y el método es virtual, a menos que el registro esté sellado o abstracto. Si el registro contenedor es abstracto, el método clonado sintetizado también es abstracto. Si el método "clone" no es abstracto, devuelve el resultado de una llamada a un constructor de copia.
Miembros de impresión: métodos PrintMembers y ToString
Si el registro se deriva de object
, el registro incluye un método sintetizado equivalente a un método declarado de la siguiente manera:
bool PrintMembers(System.Text.StringBuilder builder);
El método es el private
si el tipo de registro es sealed
. De lo contrario, el método es virtual
y protected
.
El método :
- llama al método
System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack()
si el método está presente y el registro tiene miembros que se puedan imprimir. - para cada uno de los miembros imprimibles del 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 ",";
- devuelve true si el 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 el tipo de registro se deriva de un registro base Base
, el registro incluye una sobrescritura sintetizada equivalente a un método declarado de la siguiente manera:
protected override bool PrintMembers(StringBuilder builder);
Si el registro no tiene miembros imprimibles, el método llama al método PrintMembers
base con un argumento (su parámetro builder
) y devuelve el resultado.
De lo contrario, el método:
- llama al método base
PrintMembers
con un argumento (su parámetrobuilder
) - si el método
PrintMembers
devolvió true, anexe ", " al generador, - para cada uno de los miembros imprimibles del registro, anexa el nombre del miembro seguido de " = " seguido del valor del miembro:
this.member
(othis.member.ToString()
para los tipos de valor), separados por ", ", - devuelve true.
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, o si la declaración explícita no permite reemplazarla en un tipo derivado y el tipo de registro no es sealed
.
El registro incluye un método sintetizado equivalente a un método declarado de la siguiente manera:
public override string ToString();
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 la declaración explícita no permite reemplazarla en un tipo derivado y el tipo de registro no es sealed
. Es un error si un método sintetizado o declarado explícitamente no sobrescribe object.ToString()
(por ejemplo, debido al ocultamiento en tipos base intermedios, etc.).
El método sintetizado:
- crea una instancia de
StringBuilder
, - anexa el nombre del registro al constructor, seguido de "{",
- invoca el método
PrintMembers
del registro con el constructor, seguido de " " si ha devuelto el valor true, - anexa "}",
- devuelve los contenidos del constructor con
builder.ToString()
.
Por ejemplo, considere los siguientes tipos de registro:
record R1(T1 P1);
record R2(T1 P1, T2 P2, T3 P3) : R1(P1);
Para esos tipos de registro, los miembros de impresión sintetizados podrían ser similares a:
class R1 : IEquatable<R1>
{
public T1 P1 { get; init; }
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append(nameof(P1));
builder.Append(" = ");
builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if T1 is 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();
}
}
class R2 : R1, IEquatable<R2>
{
public T2 P2 { get; init; }
public T3 P3 { get; init; }
protected override bool PrintMembers(StringBuilder builder)
{
if (base.PrintMembers(builder))
builder.Append(", ");
builder.Append(nameof(P2));
builder.Append(" = ");
builder.Append(this.P2); // or builder.Append(this.P2); if T2 is a value type
builder.Append(", ");
builder.Append(nameof(P3));
builder.Append(" = ");
builder.Append(this.P3); // or builder.Append(this.P3); if T3 is a value type
return true;
}
public override string ToString()
{
var builder = new StringBuilder();
builder.Append(nameof(R2));
builder.Append(" { ");
if (PrintMembers(builder))
builder.Append(" ");
builder.Append("}");
return builder.ToString();
}
}
Miembros posicionales del registro
Además de los miembros anteriores, los registros con una lista de parámetros ("registros posicionales") sintetizan miembros adicionales con las mismas condiciones que los miembros anteriores.
Constructor principal
Un tipo 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 para el tipo y hace que el constructor de clase predeterminado declarado implícitamente, si está presente, se suprima. Es un error tener un constructor principal y que también haya un constructor con la misma firma ya presente en la clase.
Durante el tiempo de ejecución, el constructor principal
ejecuta los inicializadores de instancia que aparecen en el cuerpo de la clase
invoca el constructor de clase base con los argumentos proporcionados en la cláusula
record_base
, si está presente.
Si un registro tiene un constructor principal, cualquier constructor definido por el usuario, excepto "constructor de copia" debe tener un inicializador de constructor this
explícito.
Los parámetros del constructor principal, así como los miembros del registro, están disponibles en el ámbito de argument_list
de la cláusula record_base
y en los inicializadores de los 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 se pueden usar, de forma similar a cómo funcionan las llamadas base y los inicializadores en constructores normales en la actualidad.
Se genera una advertencia si no se lee un parámetro del constructor principal.
Las variables de expresión declaradas en el argument_list
están dentro del ámbito de la argument_list
. Se aplican las mismas reglas de sombreado que dentro de una lista de argumentos de un inicializador de constructor normal.
Propiedades
Para cada parámetro de registro de una declaración de tipo de registro hay un miembro de propiedad pública correspondiente cuyo nombre y tipo se toman de la declaración de parámetro de valor.
Para que conste:
- Se crea una propiedad automática y
get
públicainit
(consulte la especificación independiente del descriptor de accesoinit
). Una propiedadabstract
heredada con el mismo tipo se invalida. Es un error que la propiedad heredada no tenga descriptores de accesopublic
get
yinit
reemplazables. Se trata de un error si la propiedad heredada está oculta.
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 destinosproperty:
ofield:
para los atributos sintácticamente aplicados al parámetro del registro correspondiente.
Deconstruct
Un registro posicional con al menos un parámetro síntetiza un método de instancia público que devuelve void llamado Deconstruct, con una declaración de parámetro 'out' para 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 de la propiedad de instancia del mismo nombre.
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.
En el ejemplo siguiente se muestra un registro posicional R
con su método de Deconstruct
sintetizado del compilador, junto con su uso:
public record R(int P1, string P2 = "xyz")
{
public void Deconstruct(out int P1, out string P2)
{
P1 = this.P1;
P2 = this.P2;
}
}
class Program
{
static void Main()
{
R r = new R(12);
(int p1, string p2) = r;
Console.WriteLine($"p1: {p1}, p2: {p2}");
}
}
Expresión with
Una expresión with
es una expresión nueva con la siguiente sintaxis.
with_expression
: switch_expression
| switch_expression 'with' '{' member_initializer_list? '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: identifier '=' expression
;
No se permite una expresión with
como declaración.
Una expresión with
permite una "mutación no destructiva", diseñada para generar una copia de la expresión receptora con modificaciones en las asignaciones de la member_initializer_list
.
Una expresión with
válida tiene un receptor con un tipo no nulo. El tipo de receptor debe ser un registro.
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 primer lugar, se invoca el método "clone" del receptor (especificado anteriormente) y su resultado se convierte en el tipo del receptor. A continuación, cada member_initializer
se procesa de la misma manera que una asignación a un campo o acceso a una propiedad del resultado de la conversión. Las asignaciones se procesan en orden léxico.
C# feature specifications