Constructores principales
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 .
Resumen
Las clases y estructuras pueden tener una lista de parámetros y su especificación de clase base puede tener una lista de argumentos. Los parámetros del constructor principal están en el ámbito de toda la declaración de clase o estructura y, si son capturados por un miembro de función o una función anónima, se almacenan adecuadamente (por ejemplo, como campos privados inexobles de la clase o estructura declaradas).
La propuesta "reconvierte" los constructores primarios ya disponibles en los registros en términos de esta característica más general con algunos miembros adicionales sintetizados.
Motivación
La capacidad de una clase o estructura en C# para tener más de un constructor proporciona generalidad, pero a costa de algún tedium en la sintaxis de declaración, ya que la entrada del constructor y el estado de clase deben estar separadas limpiamente.
Los constructores principales colocan los parámetros de un constructor en el ámbito de toda la clase o estructura que se va a usar para la inicialización o directamente como estado de objeto. La contrapartida es que cualquier otro constructor debe llamar a través del constructor primario.
public class B(bool b) { } // base class
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(S));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
Diseño detallado
Esto describe el diseño generalizado a través de registros y no registros, y luego detalla cómo los constructores primarios existentes para los registros se especifican mediante la adición de un conjunto de miembros sintetizados en presencia de un constructor primario.
Sintaxis
Las declaraciones de clase y estructura se aumentan para permitir una lista de parámetros en el nombre de tipo, una lista de argumentos en la clase base y un cuerpo que consta de solo un ;
:
class_declaration
: attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
parameter_list? class_base? type_parameter_constraints_clause* class_body
;
class_designator
: 'record' 'class'?
| 'class'
class_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? ',' interface_type_list
;
class_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
struct_declaration
: attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
;
struct_body
: '{' struct_member_declaration* '}' ';'?
| ';'
;
interface_declaration
: attributes? interface_modifier* 'partial'? 'interface'
identifier variant_type_parameter_list? interface_base?
type_parameter_constraints_clause* interface_body
;
interface_body
: '{' interface_member_declaration* '}' ';'?
| ';'
;
enum_declaration
: attributes? enum_modifier* 'enum' identifier enum_base? enum_body
;
enum_body
: '{' enum_member_declarations? '}' ';'?
| '{' enum_member_declarations ',' '}' ';'?
| ';'
;
Nota: Estas producciones reemplazan record_declaration
en Records y record_struct_declaration
en Record structs , las cuales ambas se vuelven obsoletas.
Es un error que un class_base
tenga un argument_list
si el class_declaration
que lo contiene no contiene un parameter_list
. Como máximo, una declaración de tipo parcial de una clase o estructura parcial puede proporcionar un parameter_list
. Los parámetros parameter_list
de una declaración record
deben ser todos parámetros de valor.
Tenga en cuenta que, según esta propuesta class_body
, struct_body
, interface_body
y enum_body
pueden constar solo de un ;
.
Una clase o estructura con un parameter_list
tiene un constructor público implícito 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 sin parámetros declarado implícitamente, si está presente, se suprima. Es un error tener un constructor primario y un constructor con la misma firma ya presentes en la declaración de tipo.
Búsqueda
La búsqueda de nombres simples se amplía para controlar parámetros de constructores primarios. Los cambios se resaltan en negrita en el fragmento siguiente:
- En caso contrario, para cada tipo de instancia
T
(§15.3.2), empezando por el tipo de instancia de la declaración de tipo inmediatamente contigua y continuando con el tipo de instancia de cada declaración de clase o estructura contigua (si existe):
- Si la declaración de
T
incluye un parámetro constructor primarioI
y la referencia ocurre dentro deargument_list
deT
declass_base
o dentro de un inicializador de un campo, propiedad o evento deT
, el resultado es el parámetro constructor primarioI
.- De lo contrario, si
e
es cero y la declaración deT
incluye un parámetro de tipo con nombreI
, el simple_name hace referencia a ese parámetro de tipo.- De lo contrario, si una búsqueda de miembros (§12.5) de
I
enT
con argumentos de tipoe
da como resultado una coincidencia:
- Si
T
es el tipo de instancia de la clase o estructura envolvente inmediatamente y la búsqueda identifica uno o varios métodos, el resultado es un grupo de métodos con una expresión de instancia asociada dethis
. Si se especificó una lista de argumentos de tipo, se usa para llamar a un método genérico (§12.8.10.2).- De otra manera, si
T
es el tipo de instancia de la clase o el tipo de estructura inmediatamente envolvente, si la búsqueda identifica un miembro de instancia y si la referencia se produce dentro del bloque de un constructor de instancia, un método de instancia o un descriptor de acceso de instancia (§12.2.1), el resultado es el mismo que un acceso de miembro (§12.8.7) de la formathis.I
. Esto solo puede ocurrir cuandoe
es cero.- De lo contrario, el resultado es el mismo que un acceso de miembro (§12.8.7) del formulario
T.I
oT.I<A₁, ..., Aₑ>
.- De lo contrario, si la declaración de
T
incluye un parámetro de constructor principalI
, el resultado es el parámetro del constructor principalI
.
La primera adición corresponde al cambio provocado por los constructores principales en los registros, y garantiza que los parámetros del constructor principal se encuentren antes que los campos correspondientes dentro de los inicializadores y los argumentos de la clase base. Extiende esta regla también a inicializadores estáticos. Sin embargo, dado que los registros siempre tienen un miembro de instancia con el mismo nombre que el parámetro , la extensión solo puede provocar un cambio en un mensaje de error. Acceso no válido a un parámetro frente al acceso no válido a un miembro de instancia.
La segunda adición permite que los parámetros primarios del constructor se encuentren en cualquier otra parte del cuerpo del tipo, pero solo si no están ocultos por miembros.
Se trata de un error al hacer referencia a un parámetro de constructor principal si la referencia no se produce dentro de una de las siguientes opciones:
- un argumento
nameof
- un inicializador de un campo de instancia, propiedad o evento del tipo declarante (tipo declarante constructor primario con el parámetro).
- el
argument_list
declass_base
del tipo declarante. - el cuerpo de un método de instancia (tenga en cuenta que se excluyen los constructores de instancia) del tipo declarativo.
- el cuerpo de un descriptor de acceso de instancia del tipo declarante.
En otras palabras, los parámetros del constructor primario están en el ámbito de todo el cuerpo del tipo declarante. Sombrean a miembros del tipo declarante dentro de un inicializador de un campo, propiedad o evento del tipo declarante, o dentro del argument_list
de class_base
del tipo declarante. Los miembros del tipo declarante les hacen sombra en cualquier otro lugar.
Así, en la siguiente declaración:
class C(int i)
{
protected int i = i; // references parameter
public int I => i; // references field
}
El inicializador del campo i
hace referencia al parámetro i
, mientras que el cuerpo de la propiedad I
hace referencia al campo i
.
Advertir sobre el seguimiento por parte de un miembro de base
El compilador generará una advertencia sobre el uso de un identificador cuando un miembro base sombree un parámetro de constructor principal si ese parámetro de constructor principal no se pasó al tipo base a través de su constructor.
Se considera que un parámetro de constructor principal se pasa al tipo base a través de su constructor cuando se cumplen todas las condiciones siguientes para un argumento en class_base:
- El argumento representa una conversión de identidad implícita o explícita de un parámetro de constructor principal;
- El argumento no forma parte de un argumento expandido
params
;
Semántica
Un constructor principal provoca la generación de un constructor de instancia en el tipo contenedor con los parámetros especificados. Si el class_base
tiene una lista de argumentos, el constructor de instancia generado tendrá un inicializador base
con la misma lista de argumentos.
Los parámetros de constructor principal en las declaraciones de clase o estructura se pueden declarar ref
, in
o out
. La declaración de los parámetros ref
o out
sigue siendo ilegal en los constructores primarios de la declaración de registro.
Todos los inicializadores de miembros de instancia en el cuerpo de la clase se convertirán en asignaciones en el constructor generado.
Si se hace referencia a un parámetro del constructor principal desde dentro de un miembro de instancia y la referencia no está dentro de un argumento de nameof
, se captura en el estado del tipo contenedor, de modo que permanezca accesible tras la terminación del constructor. Una estrategia de implementación probable es a través de un campo privado utilizando un nombre alterado. En una estructura de solo lectura, los campos de captura serán de solo lectura. Por lo tanto, el acceso a los parámetros capturados de una estructura de solo lectura tendrá restricciones similares como acceso a campos de solo lectura. El acceso a los parámetros capturados dentro de un miembro readonly tendrá restricciones similares al acceso a los campos de instancia en el mismo contexto.
No se permite la captura para parámetros de tipo ref, ni para los parámetros ref
, in
o out
. Esto es similar a una limitación para la captura en lambdas.
Si solo se hace referencia a un parámetro de constructor principal desde inicializadores de miembro de instancia, estos pueden hacer referencia directamente al parámetro del constructor generado, ya que se ejecutan como parte de él.
El constructor principal realizará la siguiente secuencia de operaciones:
- Los valores de parámetro se almacenan en campos de captura, si los hay.
- Se ejecutan inicializadores de instancia
- El inicializador del constructor base es llamado
Las referencias de parámetros en cualquier código de usuario se reemplazan por las referencias de campo de captura correspondientes.
Por ejemplo, esta declaración:
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
Genera código similar al siguiente:
public class C : B
{
public int I { get; set; }
public string S
{
get => __s;
set => __s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(0, s) { ... } // must call this(...)
// generated members
private string __s; // for capture of s
public C(bool b, int i, string s)
{
__s = s; // capture s
I = i; // run I's initializer
B(b) // run B's constructor
}
}
Es un error que una declaración de constructor no principal tenga la misma lista de parámetros que el constructor principal. Todas las declaraciones de constructores no principales deben utilizar un inicializador this
, para que al final se llame al constructor principal.
Los registros generan una advertencia si no se lee un parámetro de constructor principal dentro de los inicializadores de instancia (posiblemente generados) o inicializadores base. Se notificarán advertencias similares para los parámetros del constructor principal en clases y estructuras:
- para un parámetro by-value, si el parámetro no es capturado y no es leído dentro de ningún inicializador de instancia o inicializador base.
- para un parámetro
in
, si el parámetro no se lee dentro de ningún inicializador de instancia o inicializador base. - para un parámetro
ref
, si el parámetro no es leído o escrito dentro de ningún inicializador de instancia o inicializador base.
Nombres simples idénticos y nombres de tipo
Existe una regla especial del lenguaje para escenarios a menudo denominados "Color Color": Nombres simples y nombres de tipo idénticos.
En un acceso de miembro de la forma
E.I
, siE
es un identificador único y si el significado deE
como un simple_name (§12.8.4) es una constante, campo, propiedad, variable local o parámetro con el mismo tipo que el significado deE
como un type_name (§7.8.1), se permiten ambos significados posibles deE
. La búsqueda de miembros deE.I
nunca es ambigua, ya queI
será necesariamente un miembro del tipoE
en ambos casos. En otras palabras, la regla simplemente permite el acceso a los miembros estáticos y los tipos anidados deE
, de lo contrario, se produciría un error en tiempo de compilación.
Con respecto a los constructores primarios, la regla afecta si un identificador dentro de un miembro de instancia debe tratarse como una referencia de tipo o como una referencia de parámetro de un constructor principal, que, a su vez, captura dicho parámetro en el estado del tipo que lo contiene. Aunque "la búsqueda de miembros de E.I
nunca es ambigua", cuando la búsqueda da como resultado un grupo de miembros, en algunos casos es imposible determinar si un acceso de miembro se refiere a un miembro estático o a un miembro de instancia sin resolver completamente (vincular) el acceso de miembro. Al mismo tiempo, la captura de un parámetro de constructor principal cambia las propiedades del tipo envolvente de una manera que afecta al análisis semántico. Por ejemplo, es posible que el tipo no esté administrado y que, por ello, no cumpla determinadas restricciones.
Incluso hay situaciones en las que la vinculación puede tener éxito en ambos sentidos, dependiendo de si el parámetro se considera capturado o no. Por ejemplo:
struct S1(Color Color)
{
public void Test()
{
Color.M1(this); // Error: ambiguity between parameter and typename
}
}
class Color
{
public void M1<T>(T x, int y = 0)
{
System.Console.WriteLine("instance");
}
public static void M1<T>(T x) where T : unmanaged
{
System.Console.WriteLine("static");
}
}
Si tratamos al receptor Color
como un valor, capturamos el parámetro y "S1" pasa a ser gestionado. A continuación, el método estático se vuelve inaplicable debido a la restricción y llamaríamos al método de instancia. Sin embargo, si tratamos al receptor como un tipo, no capturamos el parámetro y 'S1' permanece no administrado, ambos métodos son aplicables, pero el método estático es "mejor" porque no tiene un parámetro opcional. Ninguna de las opciones conduce a un error, pero cada una daría lugar a un comportamiento distinto.
Dado esto, el compilador producirá un error de ambigüedad para un acceso a miembro E.I
cuando se cumplan todas las condiciones siguientes:
- La búsqueda de miembros de
E.I
produce un grupo de miembros que contiene miembros de instancia y estáticos al mismo tiempo. Los métodos de extensión aplicables al tipo de receptor se tratan como métodos de instancia con el fin de esta comprobación. - Si
E
se trata como un simple nombre, en lugar de un nombre de tipo, se referiría a un parámetro constructor primario y capturaría el parámetro en el estado del tipo que lo encierra.
Advertencias de almacenamiento doble
Si un parámetro del constructor principal se pasa a la base y también se capturan y, existe un alto riesgo de que se almacene accidentalmente dos veces en el objeto.
El compilador generará una advertencia para in
o por argumento de valor en un class_base
argument_list
cuando se cumplen todas las condiciones siguientes:
- El argumento representa una conversión de identidad implícita o explícita de un parámetro de constructor principal;
- El argumento no forma parte de un argumento expandido
params
; - El parámetro del constructor primario se captura en el estado del tipo que lo contiene.
El compilador generará una advertencia para un variable_initializer
cuando se cumplen todas las condiciones siguientes:
- El inicializador de variable representa una conversión de identidad implícita o explícita de un parámetro de constructor principal;
- El parámetro del constructor primario se captura en el estado del tipo que lo contiene.
Por ejemplo:
public class Person(string name)
{
public string Name { get; set; } = name; // warning: initialization
public override string ToString() => name; // capture
}
Atributos dirigidos a constructores primarios
En https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md decidimos adoptar la propuesta de https://github.com/dotnet/csharplang/issues/7047.
El destino del atributo "method" se permite en una declaración_de_clase /declaración_de_estructura con lista_de_parámetros y como resultado, el constructor principal correspondiente tiene ese atributo.
Los atributos con el objetivo method
en una class_declaration/struct_declaration sin parameter_list se ignoran con una advertencia.
[method: FooAttr] // Good
public partial record Rec(
[property: Foo] int X,
[field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
public void Frobnicate()
{
...
}
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;
Constructores primarios en registros
Con esta propuesta, los registros ya no necesitan especificar por separado un mecanismo de constructor principal. En su lugar, las declaraciones de registro (clase y estructura) que tienen constructores principales seguirían las reglas generales, con estas adiciones simples:
- Para cada parámetro de constructor principal, si ya existe un miembro con el mismo nombre, debe ser una propiedad o campo de instancia. Si no, se sintetiza una autopropiedad pública init-only del mismo nombre con un inicializador de propiedad que asigna desde el parámetro.
- Un deconstructor se sintetiza con parámetros out que coinciden con los parámetros del constructor primario.
- Si una declaración explícita de constructor es un "constructor de copia" - un constructor que toma un único parámetro del tipo envolvente - no se requiere llamar a un inicializador
this
, y no ejecutará los inicializadores de miembro presentes en la declaración de registro.
Inconvenientes
- El tamaño de asignación de objetos construidos es menos obvio, ya que el compilador determina si se debe asignar un campo para un parámetro de constructor principal basado en el texto completo de la clase. Este riesgo es similar a la captura implícita de variables por expresiones lambda.
- Una tentación común (o patrón accidental) podría ser capturar el mismo parámetro en varios niveles de herencia a medida que se pasa por la cadena de constructores en lugar de asignarle explícitamente un campo protegido en la clase base, lo que provoca asignaciones duplicadas para los mismos datos en objetos. Esto es muy similar al riesgo actual de anular autopropiedades con autopropiedades.
- Como se propone aquí, no hay ningún lugar para la lógica adicional que normalmente puede expresarse en cuerpos constructores. La extensión "cuerpos de constructores primarios" que aparece a continuación aborda esta cuestión.
- Tal y como se propone, la semántica del orden de ejecución es sutilmente diferente de dentro de los constructores ordinarios, retrasando los inicializadores de miembros a después de las llamadas base. Esto probablemente podría remediarse, pero a costa de algunas de las propuestas de extensión (especialmente "cuerpos de constructores primarios").
- La propuesta solo funciona en escenarios en los que se puede designar un único constructor principal.
- No hay ninguna manera de expresar la accesibilidad independiente de la clase y el constructor principal. Un ejemplo es cuando todos los constructores públicos delegan a un constructor privado que lo construye todo. Si es necesario, se podría proponer sintaxis para eso más adelante.
Alternativas
Sin captura
Una versión mucho más simple de la función prohibiría que los parámetros de los constructores primarios aparecieran en los cuerpos de los miembros. Hacer referencia a ellos sería un error. Los campos tendrían que declararse explícitamente si el almacenamiento se desea más allá del código de inicialización.
public class C(string s)
{
public string S1 => s; // Nope!
public string S2 { get; } = s; // Still allowed
}
Esto podría evolucionar a la propuesta completa más adelante, y evitaría una serie de decisiones y complejidades, a costa de eliminar menos repeticiones inicialmente, y probablemente también de parecer poco intuitivo.
Campos generados explícitamente
Un enfoque alternativo consiste en que los parámetros del constructor principal generen siempre y de manera visible un campo con el mismo nombre. En lugar de cerrar sobre los parámetros de la misma manera que las funciones locales y anónimas, habría explícitamente una declaración de miembro generada, similar a las propiedades públicas generadas para los parámetros constructores primarios en los registros. Al igual que para los registros, si ya existe un miembro adecuado, no se generaría uno.
Si el campo generado es privado, podría seguir elidiéndose cuando no se utilice como campo en los cuerpos de los miembros. En las clases, sin embargo, un campo privado a menudo no sería la elección correcta, debido a la duplicación de estados que podría causar en las clases derivadas. Una opción aquí sería generar un campo protegido en clases, lo que fomentaría la reutilización del almacenamiento entre capas de herencia. Sin embargo, entonces no podríamos eludir la declaración y incurriría en costos de asignación para cada parámetro de constructor principal.
Esto alinearía más estrechamente los constructores primarios que no son registros con los que sí lo son, en el sentido de que siempre se generan miembros (al menos conceptualmente), aunque se trate de diferentes tipos de miembros con diferentes accesibilidades. Pero también daría lugar a diferencias sorprendentes de cómo se capturan los parámetros y las variables locales en otras partes de C#. Si alguna vez se permitían clases locales, por ejemplo, capturarían parámetros envolventes y variables locales implícitamente. La generación visible de campos de sombra para ellos no parece ser un comportamiento razonable.
Otro problema que se suele producir con este enfoque es que muchos desarrolladores tienen convenciones de nomenclatura diferentes para parámetros y campos. ¿Qué se debe usar para el parámetro del constructor principal? Cualquiera de las opciones provocaría incoherencia con el resto del código.
Por último, la generación visible de declaraciones de miembros es lo normal en el caso de los registros, pero mucho más sorprendente y "fuera de lugar" en el caso de las clases y los structs que no son registros. Todas ellas son las razones por las que la propuesta principal opta por la captura implícita, con un comportamiento razonable (coherente con los registros) para declaraciones de miembros explícitas cuando se desean.
Eliminación de miembros de instancia del ámbito del inicializador
Las reglas de búsqueda anteriores están pensadas para permitir el comportamiento actual de los parámetros del constructor principal en los registros cuando se declara manualmente un miembro correspondiente y explicar el comportamiento del miembro generado cuando no lo es. Esto requiere que la búsqueda difiera entre "ámbito de inicialización" (inicializadores this/base, inicializadores de miembros) y "ámbito de cuerpo" (cuerpos de miembros), lo que la propuesta anterior consigue cambiando cuándo se buscan los parámetros del constructor primario, dependiendo de dónde se produzca la referencia.
Una observación es que hacer referencia a un miembro de instancia con un nombre simple en el ámbito del inicializador siempre produce un error. En lugar de simplemente hacer sombra a los miembros de instancia en esos lugares, ¿podríamos simplemente sacarlos del ámbito? De este modo, no habría este orden condicional extraño de ámbitos.
Es probable que esta alternativa sea posible, pero tendría algunas consecuencias que son un poco de alcance y potencialmente no deseadas. En primer lugar, si eliminamos los miembros de instancia del ámbito del inicializador, un nombre simple que corresponda a un miembro de instancia y no a un parámetro primario del constructor podría enlazarse accidentalmente a algo fuera de la declaración de tipo. Esto parece que rara vez sería intencional y un error sería mejor.
Además, está bien hacer referencia a los miembros estáticos en el ámbito de inicialización. Por lo tanto, tendríamos que distinguir entre los miembros estáticos e de instancia en la búsqueda, algo que no hacemos hoy. (Distinguimos en la resolución de sobrecargas, aunque eso no se aplica aquí). Así que eso también tendría que cambiarse, lo que llevaría a más situaciones en las que, por ejemplo, en contextos estáticos algo se enlazaría "más allá" en lugar de dar error porque encontró un miembro de instancia.
En definitiva, esta "simplificación" llevaría a una complicación que nadie pidió.
Extensiones posibles
Estas son variaciones o adiciones a la propuesta básica que se pueden considerar junto con ella, o en una fase posterior si se considera útil.
Acceso a los parámetros del constructor primario dentro de los constructores
Las reglas anteriores consideran un error referenciar un parámetro de un constructor principal dentro de otro constructor. Esto podría permitirse dentro del cuerpo de otros constructores, ya que el constructor primario se ejecuta primero. Sin embargo, tendría que seguir estando prohibido dentro de la lista de argumentos del inicializador this
.
public class C(bool b, int i, string s) : B(b)
{
public C(string s) : this(b, s) // b still disallowed
{
i++; // could be allowed
}
}
Tal acceso aún incurriría en captura, ya que sería la única manera de que el cuerpo del constructor pudiera acceder a la variable después de que el constructor primario ya se haya ejecutado.
La prohibición de parámetros de constructor primario en los argumentos del inicializador this podría debilitarse para permitirlos, pero haciendo que no se asignen definitivamente, pero eso no parece útil.
Permitir constructores sin inicializador this
Se podrían permitir constructores sin inicializador this
(es decir, con un inicializador base
implícito o explícito). Un constructor de este tipo no ejecutaría inicializadores de campos de instancia, propiedades y eventos, ya que estos se considerarían solo parte del constructor primario.
En presencia de constructores que invocan bases, hay un par de opciones sobre cómo se maneja la captura de parámetros del constructor principal. Lo más sencillo es no permitir completamente la captura en esta situación. Los parámetros de constructor principal solo serían para la inicialización cuando existen dichos constructores.
Como alternativa, si se combina con la opción descrita anteriormente para permitir el acceso a los parámetros del constructor principal dentro de los constructores, los parámetros podrían entrar en el cuerpo del constructor como no asignados definitivamente, y aquellos que se capturan necesitarían estar definitivamente asignados al final del cuerpo del constructor. Básicamente, serían parámetros de salida implícitos. De este modo, los parámetros de constructor principal capturados siempre tendrían un valor razonable (es decir, asignado explícitamente) en el momento en que son consumidos por otros miembros de función.
Un atractivo de esta extensión (en cualquiera de sus formas) es que generaliza completamente la exención actual para "constructores de copia" en los registros, sin llevar a situaciones en las que se observen parámetros de constructores primarios no inicializados. Básicamente, los constructores que inicializan el objeto de maneras alternativas son correctos. Las restricciones relacionadas con la captura no serían un cambio importante para los constructores de copia definidos manualmente en los registros, ya que los registros nunca capturan sus parámetros de constructor principal (generan campos en su lugar).
public class C(bool b, int i, string s) : B(b)
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s2) : base(true) // cannot use `string s` because it would shadow
{
s = s2; // must initialize s because it is captured by S
}
protected C(C original) : base(original) // copy constructor
{
this.s = original.s; // assignment to b and i not required because not captured
}
}
Cuerpos de constructores primarios
Los propios constructores suelen contener lógica de validación de parámetros u otro código de inicialización notrivial que no se puede expresar como inicializadores.
Los constructores primarios podrían ampliarse para permitir que los bloques de sentencias aparezcan directamente en el cuerpo de la clase. Esas declaraciones se insertarían en el constructor generado en el punto en el que aparecen entre las asignaciones de inicialización y, por tanto, se ejecutarían intercaladas con inicializadores. Por ejemplo:
public class C(int i, string s) : B(s)
{
{
if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
}
int[] a = new int[i];
public int S => s;
}
Gran parte de este escenario podría cubrirse adecuadamente si introdujéramos "inicializadores finales" que se ejecuten después de que los constructores y cualquier inicializador de objeto/colección hayan finalizado. Sin embargo, la validación de argumentos es una cosa que idealmente ocurriría lo antes posible.
Los cuerpos de los constructores primarios también podrían proporcionar un lugar para permitir un modificador de acceso para el constructor primario, permitiéndole desviarse de la accesibilidad del tipo que lo encierra.
Declaraciones combinadas de parámetros y miembros
Una posible y a menudo mencionada adición podría ser permitir que los parámetros del constructor primario sean anotados para que también declaren un miembro en el tipo. Normalmente, se propone permitir que un especificador de acceso en los parámetros desencadene la generación de miembros:
public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
void M()
{
... i ... // refers to the field i
... s ... // closes over the parameter s
}
}
Hay algunos problemas:
- ¿Qué ocurre si se desea una propiedad, no un campo? Tener la sintaxis
{ get; set; }
en línea en una lista de parámetros no parece apetecible. - ¿Qué ocurre si se usan diferentes convenciones de nomenclatura para parámetros y campos? Entonces esta característica sería inútil.
Se trata de una adición futura potencial que se puede adoptar o no. La propuesta actual deja abierta la posibilidad.
Preguntas abiertas
Orden de búsqueda de parámetros de tipo
La sección https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup especifica que los parámetros del tipo declarante deben ir antes que los parámetros del constructor primario del tipo en todos los contextos en los que esos parámetros estén en el ámbito. Sin embargo, ya tenemos un comportamiento existente con registros: los parámetros de constructor principal vienen antes de los parámetros de tipo en inicializadores base y inicializadores de campo.
¿Qué debemos hacer sobre esta discrepancia?
- Ajuste las reglas para que coincidan con el comportamiento.
- Ajustar el comportamiento (un posible cambio de ruptura).
- No permitir que un parámetro de constructor primario utilice el nombre del parámetro de tipo (un posible cambio de ruptura).
- No haga nada, acepte la incoherencia entre la especificación y la implementación.
Conclusión:
Ajuste las reglas para que coincidan con el comportamiento (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).
Atributos de orientación de campo para parámetros constructores primarios capturados
¿Se deben permitir atributos de destino de campo para los parámetros de constructor principal capturados?
class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = x;
}
En este momento, los atributos se omiten con la advertencia, independientemente de si se captura el parámetro.
Tenga en cuenta que para los registros, se permiten atributos específicos de campo cuando se sintetiza una propiedad para dicho registro. Los atributos van en el campo de respaldo entonces.
record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = X;
}
Conclusión:
No permitido (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).
Advertir sobre el seguimiento por parte de un miembro de base
¿Deberíamos notificar una advertencia cuando un miembro de la base sombrea un parámetro de constructor principal dentro de un miembro (vea https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?
Conclusión:
Se aprueba un diseño alternativo: https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
Captura de instancia del tipo envolvente en un cierre
Cuando un parámetro capturado en el estado del tipo envolvente es también referenciado en un lambda dentro de un inicializador de instancia o un inicializador base, el lambda y el estado del tipo envolvente deberían referirse a la misma localización para el parámetro. Por ejemplo:
partial class C1
{
public System.Func<int> F1 = Execute1(() => p1++);
}
partial class C1 (int p1)
{
public int M1() { return p1++; }
static System.Func<int> Execute1(System.Func<int> f)
{
_ = f();
return f;
}
}
Dado que la implementación ingenua de capturar un parámetro en el estado del tipo simplemente captura el parámetro en un campo de instancia privado, la lambda necesita referirse al mismo campo. Como consecuencia, debe ser capaz de acceder a la instancia del tipo. Esto requiere la captura this
en un cierre antes de que el constructor base sea invocado. Eso, a su vez, da como resultado una IL segura, pero no verificable. ¿Esto es aceptable?
Como alternativa, podríamos:
- No permita lambdas como esa;
- O, en su lugar, capturar parámetros como ese en una instancia de una clase separada (otro cierre), y compartir esa instancia entre el cierre y la instancia del tipo que lo encierra. Así se elimina la necesidad de capturar
this
en un cierre.
Conclusión:
Nos sentimos cómodos con la captura this
en un cierre antes de que el constructor base sea invocado (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).
El equipo en tiempo de ejecución tampoco encontró el patrón IL problemático.
Asignar a this
dentro de una estructura
C# permite asignar a this
dentro de una estructura. Si el struct captura un parámetro de constructor principal, la asignación sobrescribirá su valor, lo que podría no ser obvio para el usuario. ¿Queremos reportar una advertencia para asignaciones como esta?
struct S(int x)
{
int X => x;
void M(S s)
{
this = s; // 'x' is overwritten
}
}
Conclusión:
Permitido, sin advertencia (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).
Advertencia de doble almacenamiento para inicialización más captura
Tenemos una advertencia si un parámetro del constructor principal se pasa a la base y tanto como son capturados, ya que hay un alto riesgo de que se almacene involuntariamente dos veces en el objeto.
Parece que existe un riesgo similar si se utiliza un parámetro para inicializar un miembro y también se captura. Este es un ejemplo pequeño:
public class Person(string name)
{
public string Name { get; set; } = name; // initialization
public override string ToString() => name; // capture
}
Para una instancia específica de Person
, los cambios en Name
no se reflejarían en la salida de ToString
, lo que probablemente no fue la intención del desarrollador.
¿Deberíamos introducir una advertencia de doble almacenamiento para esta situación?
Así es como funcionaría:
El compilador generará una advertencia para un variable_initializer
cuando se cumplen todas las condiciones siguientes:
- El inicializador de variable representa una conversión de identidad implícita o explícita de un parámetro de constructor principal;
- El parámetro del constructor primario se captura en el estado del tipo que lo contiene.
Conclusión:
Aprobado, consulte https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
Reuniones LDM
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-10-17.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-01-18.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-22.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors
C# feature specifications