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 y 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 especificaciones.
Problema planteado por el experto: https://github.com/dotnet/csharplang/issues/2691
Resumen
Las clases y estructuras pueden tener una lista de parámetros, y la especificación de su clase base puede tener una lista de argumentos. Los parámetros de los constructores primarios están en el ámbito de toda la declaración de la clase o estructura, y si son capturados por un miembro de función o función anónima, se almacenan adecuadamente (por ejemplo, como campos privados indecibles de la clase o estructura declarada).
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 expensas de algo de tedio en la sintaxis de la declaración, porque la entrada del constructor y el estado de la clase necesitan estar limpiamente separados.
Los constructores primarios ponen los parámetros de un constructor en el ámbito de toda la clase o estructura para ser utilizados para la inicialización o directamente como estado del 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 del tipo, una lista de argumentos en la clase base, y un cuerpo consistente solo en 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.
Nótese que, según esta propuesta, se permite que class_body
, struct_body
, interface_body
y enum_body
consten 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 del tipo. Esto se llama el constructor primario para el tipo, y hace que el constructor sin parámetros declarado implícitamente, si está presente, sea suprimido. 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 siguiente extracto:
- 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
, entonces el simple_name se refiere 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 tipo struct que lo encierra inmediatamente y la búsqueda identifica uno o más métodos, el resultado es un grupo de métodos con una expresión de instancia asociada dethis
. Si se ha especificado una lista de argumentos de tipo, se utiliza para llamar a un método genérico (sección 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ₑ>
.- En caso contrario, si la declaración de
T
incluye un parámetro de constructor primarioI
, el resultado es el parámetro de constructor primarioI
.
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 los 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 conducir a un cambio en un mensaje de error. Acceso ilegal a un parámetro frente a acceso ilegal 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.
Es un error hacer referencia a un parámetro de constructor primario si la referencia no se produce dentro de uno de los siguientes:
- 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 (nótese que se excluyen los constructores de instancia) del tipo declarante.
- 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 producirá una advertencia sobre el uso de un identificador cuando un miembro base haga sombra a un parámetro constructor primario si ese parámetro constructor primario no fue pasado al tipo base a través de su constructor.
Se considera que un parámetro de constructor primario se pasa al tipo base a través de su constructor cuando todas las condiciones siguientes son ciertas para un argumento en class_base:
- El argumento representa una conversión de identidad implícita o explícita de un parámetro compilador primario;
- 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 compilador de instancia generado tendrá un inicializador base
con la misma lista de argumentos.
Los parámetros de compilador primario en las declaraciones de clase/estructura pueden declararse como 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 parámetros capturados de una estructura readonly tendrá restricciones similares a las del acceso a campos readonly. El acceso a parámetros capturados dentro de un miembro readonly tendrá restricciones similares a las del acceso a 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 un parámetro de un compilador primario solo es referenciado desde dentro de inicializadores de miembros instancia, estos pueden referenciar directamente el parámetro del compilador generado, ya que se ejecutan como parte del mismo.
El compilador primario realizará la siguiente secuencia de operaciones:
- Los valores de los parámetros se almacenan en los campos de captura, si los hay.
- Se ejecutan los inicializadores de instancia
- El inicializador del constructor base es llamado
Se sustituyen las referencias a parámetros en cualquier código de usuario por las correspondientes referencias a campos de captura.
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 producen una advertencia si un parámetro compilador primario no se lee dentro de los inicializadores de instancia (posiblemente generados) o del inicializador base. Se informará de advertencias similares para parámetros de compilador primario 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 y nombres de tipo idénticos
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, capturar un parámetro compilador primario cambia las propiedades del tipo que lo encierra de forma 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. Entonces el método estático se vuelve inaplicable debido a la restricción y llamaríamos método de instancia. Sin embargo, si tratamos al receptor como un tipo, no capturamos el parámetro y "S1" permanece no administrado, entonces ambos métodos son aplicables, pero el método static es "mejor" porque no tiene un parámetro opcional. Ninguna de las dos opciones conduce a un error, pero cada una tendrí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 a efectos 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 doble almacenamiento
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 producirá una advertencia para in
o por argumento de valor en un class_base
argument_list
cuando todas las condiciones siguientes sean verdaderas:
- El argumento representa una conversión de identidad implícita o explícita de un parámetro compilador primario;
- 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 producirá una advertencia para un variable_initializer
cuando todas las condiciones siguientes sean ciertas:
- El inicializador de variable representa una conversión de identidad implícita o explícita de un parámetro de compilador primario;
- 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 aceptar la propuesta 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 compilador primario. En cambio, las declaraciones de registros (clase y estructura) que tienen compiladores primarios seguirían las reglas generales, con estas simples adiciones:
- Para cada parámetro del compilador primario, 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 los objetos construidos es menos obvio, ya que el compilador determina si asignar un campo para un parámetro de compilador primario basándose en el texto completo de la clase. Este riesgo es similar a la captura implícita de variables mediante 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.
- Tal y como se propone aquí, no hay lugar para la lógica adicional que normalmente podría expresarse en los cuerpos de los compiladores. 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 para escenarios en los que un único compilador puede ser designado como primario.
- No hay forma de expresar la accesibilidad separada de la clase y el compilador primario. Un ejemplo es cuando todos los constructores públicos delegan a un constructor privado que lo construye todo. Si fuera necesario, se podría proponer una sintaxis para ello 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. Referenciarlos sería un error. Los campos tendrían que declararse explícitamente si se desea almacenarlos 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 en el caso de 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 en este caso sería generar un campo protegido en las clases, fomentando la reutilización del almacenamiento a través de las capas de herencia. Sin embargo, entonces no podríamos elidir la declaración, e incurriríamos en un coste de asignación por cada parámetro primario del compilador.
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 con respecto a cómo se capturan los parámetros y locales en otras partes de C#. Si alguna vez permitiéramos clases locales, por ejemplo, capturarían parámetros y locales implícitamente. Generar visiblemente campos de sombra para ellos no parece un comportamiento razonable.
Otro problema que se plantea a menudo con este enfoque es que muchos desarrolladores tienen diferentes convenciones de nomenclatura para parámetros y campos. ¿Cuál debería utilizarse para el parámetro principal del compilador? Cualquiera de las dos opciones llevaría a una 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. En definitiva, estas son las razones por las que la propuesta principal opta por la captura implícita, con un comportamiento sensato (coherente con los registros) para las declaraciones explícitas de miembros cuando se deseen.
Eliminar los miembros de instancia del ámbito del inicializador
Las reglas de búsqueda anteriores pretenden permitir el comportamiento actual de los parámetros de compilador primario en los registros cuando se declara manualmente un miembro correspondiente, y explicar el comportamiento del miembro generado cuando no es así. 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 referenciar un miembro de instancia con un nombre simple en el ámbito del inicializador siempre conduce a un error. En lugar de simplemente hacer sombra a los miembros de instancia en esos lugares, ¿podríamos simplemente sacarlos del ámbito? De esta forma, no habría este extraño orden condicional de los ámbitos.
Esta alternativa es probablemente posible, pero tendría algunas consecuencias de cierto alcance y potencialmente indeseables. 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. Así que tendríamos que distinguir entre miembros estáticos e instancias en la búsqueda, algo que no hacemos hoy en día. (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ó.
Posibles extensiones
Se trata de variaciones o añadidos a la propuesta básica que pueden considerarse 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. La más sencilla es desautorizar completamente la captura en esta situación. Los parámetros de los compiladores primarios serían para inicialización solo cuando tales compiladores existieran.
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. Serían esencialmente parámetros implícitos de salida. 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. Esencialmente, los compiladores que inicializan el objeto de formas alternativas están bien. Las restricciones relacionadas con la captura no supondrían un cambio radical para los compiladores de copia definidos manualmente en los registros, ya que estos nunca capturan los parámetros de sus compiladores primarios (en su lugar generan campos).
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 compiladores a menudo contienen lógica de validación de parámetros u otro código de inicialización no trivial que no puede expresarse 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 algo 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. Comúnmente 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
}
}
Esto plantea algunos problemas:
- ¿Qué pasa si se desea una propiedad y no un campo? Tener la sintaxis
{ get; set; }
en línea en una lista de parámetros no parece apetecible. - ¿Y si se utilizan convenciones de nomenclatura diferentes para los parámetros y los campos? Entonces esta función sería inútil.
Se trata de una posible adición futura que puede adoptarse o no. La propuesta actual deja abierta esta posibilidad.
Preguntas abiertas
Orden de búsqueda para 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 los registros - los parámetros del compilador primario vienen antes que los parámetros de tipo en el inicializador base y en los inicializadores de campo.
¿Qué debemos hacer con esta discrepancia?
- Ajustar 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 hacer nada, aceptar la inconsistencia entre la especificación y la implementación.
Conclusión:
Ajustar 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
¿Deberíamos permitir atributos de orientación de campo para parámetros de compilador primario 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;
}
Ahora mismo los atributos se ignoran con la advertencia independientemente de si el parámetro se captura.
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 se permite (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 informar de una advertencia cuando un miembro de la base está haciendo sombra a un parámetro de compilador primario dentro de un miembro (consulte 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. Que, a su vez, resulta en un IL seguro, pero no verificable. ¿Es esto aceptable?
Alternativamente 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 un struct. Si el struct captura un parámetro primario del compilador, la asignación va a 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 un pequeño ejemplo:
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?
Funcionaría así:
El compilador producirá una advertencia para un variable_initializer
cuando todas las condiciones siguientes sean verdaderas:
- El inicializador de variable representa una conversión de identidad implícita o explícita de un parámetro de compilador primario;
- 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