Estructuras predeterminadas automáticas
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 .
https://github.com/dotnet/csharplang/issues/5737
Resumen
Esta característica hace que, en constructores de estructura, identifiquemos los campos que el usuario no asignó explícitamente antes de devolver o antes de usar e inicializarlos implícitamente para default
en lugar de proporcionar errores de asignación definitivas.
Motivación
Esta propuesta se genera como una posible mitigación de problemas de facilidad de uso encontrados en dotnet/csharplang#5552 y dotnet/csharplang#5635, así como abordar #5563 (todos los campos deben asignarse definitivamente, pero field
no es accesible dentro del constructor).
Desde C# 1.0, es obligatorio que los constructores de estructuras asignen definitivamente this
como si fuera un parámetro out
.
public struct S
{
public int x, y;
public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
{
}
}
Esto presenta problemas cuando los métodos "set" se definen manualmente en propiedades semiautomáticas, ya que el compilador no puede ocuparse de la asignación de la propiedad como equivalente a la asignación del campo auxiliar.
public struct S
{
public int X { get => field; set => field = value; }
public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
{
}
}
Se supone que la introducción de restricciones más precisas para establecedores, como un esquema en el que el establecedor no toma ref this
, sino que en su lugar toma out field
como parámetro, podría resultar demasiado específico e incompleto para algunos casos de uso.
Hay una dificultad importante con la que hay que lidiar que se da cuando las propiedades de struct tienen métodos "set" implementados manualmente, los usuarios a menudo deben aplicar alguna forma de "repetición," ya sea asignando varias veces o repitiendo la lógica:
struct S
{
private int _x;
public int X
{
get => _x;
set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
}
// Solution 1: assign some value in the constructor before "really" assigning through the property setter.
public S(int x)
{
_x = default;
X = x;
}
// Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
public S(int x)
{
_x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
}
}
Discusión anterior
Un grupo pequeño ha examinado este problema y ha considerado algunas soluciones posibles:
- Los usuarios deben asignar
this = default
cuando las propiedades semiautomáticas han implementado métodos "set" manualmente. Estamos de acuerdo en que esta es la solución incorrecta, ya que elimina los valores establecidos en los inicializadores de campo. - Se inicializan implícitamente todos los campos subyacentes de las propiedades automáticas y semiautomáticas.
- Esto resuelve el problema de los "establecedores de propiedades semi-automáticas" y sitúa los campos declarados explícitamente bajo reglas diferentes: "no inicialice implícitamente mis campos, pero sí inicialice implícitamente mis propiedades automáticas".
- Se habilita una manera de asignar el campo auxiliar de una propiedad semiautomática y los usuarios deben asignarla.
- Esto podría ser complicado en comparación con (2). Se supone que una propiedad automática es "automática" y tal vez esto implique la inicialización "automática" del campo. Esto podría resultar confuso si se asigna el campo subyacente mediante una asignación a la propiedad y cuando se llama al método "set" de la propiedad.
También hemos recibido comentarios de los usuarios que quieren, por ejemplo, incluir algunos inicializadores de campo en structs sin tener que asignar todas las propiedades explícitamente. Podemos resolver este problema y a su vez también el de la "propiedad semiautomática con método 'set' implementado manualmente".
struct MagnitudeVector3d
{
double X, Y, Z;
double Magnitude = 1;
public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
{
}
}
Ajustar la asignación definitiva
En lugar de realizar un análisis de asignación definitiva para plasmar errores en los campos sin asignar en this
, lo hacemos para determinar qué campos deben inicializarse implícitamente. Dicha inicialización se inserta al principio del constructor.
struct S
{
int x, y;
// Example 1
public S()
{
// ok. Compiler inserts an assignment of `this = default`.
}
// Example 2
public S()
{
// ok. Compiler inserts an assignment of `y = default`.
x = 1;
}
// Example 3
public S()
{
// valid since C# 1.0. Compiler inserts no implicit assignments.
x = 1;
y = 2;
}
// Example 4
public S(bool b)
{
// ok. Compiler inserts assignment of `this = default`.
if (b)
x = 1;
else
y = 2;
}
// Example 5
void M() { }
public S(bool b)
{
// ok. Compiler inserts assignment of `y = default`.
x = 1;
if (b)
M();
y = 2;
}
}
En los ejemplos (4) y (5), el codegen resultante a veces tiene "asignaciones dobles" de campos. Esto suele ser válido, pero para los usuarios a quienes les preocupan estas asignaciones dobles, podemos mostrar lo que solían ser diagnósticos de errores de asignación definitivos como diagnósticos de advertencia de disabled-by-default (deshabilitado de forma predeterminada).
struct S
{
int x;
public S() // warning: 'S.x' is implicitly initialized to 'default'.
{
}
}
Los usuarios que establezcan la gravedad de este diagnóstico en "error" participarán en el comportamiento anterior a C# 11. Estos usuarios son básicamente "excluidos" de las propiedades semiautomáticas con métodos "set" implementados manualmente.
struct S
{
public int X
{
get => field;
set => field = field < value ? value : field;
}
public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
{
X = 1;
}
}
A primera vista, esto podría parecer una "defecto" de esta funcionalidad, pero es realmente lo ideal y conveniente. Al habilitar el diagnóstico, el usuario nos indica que no quiere que el compilador inicialice implícitamente sus campos en el constructor. Aquí no hay ninguna manera de evitar la inicialización implícita, por lo que la solución para ellos es usar una manera diferente de inicializar el campo que un establecedor implementado manualmente, como declarar manualmente el campo y asignarlo, o incluir un inicializador de campo.
Actualmente, el JIT no elimina los almacenes inactivos a través de referencias, lo que significa que estas inicializaciones implícitas tienen un costo real. Pero eso podría ser arreglable. https://github.com/dotnet/runtime/issues/13727
Vale la pena tener en cuenta que inicializar campos individuales en lugar de toda la instancia es realmente una optimización. Es probable que el compilador tenga libertad para implementar la heurística que quiera, siempre y cuando cumpla con la invariante de que los campos que no están asignados definitivamente en todos los puntos de retorno o antes de cualquier acceso a miembros no de campo de this
sean inicializados implícitamente.
Por ejemplo, si una estructura tiene 100 campos y solo se inicializa explícitamente uno de ellos, puede tener más sentido realizar un initobj
en toda la estructura, que emitir implícitamente initobj
para los otros 99 campos. Sin embargo, una implementación que emita implícitamente initobj
para los otros 99 campos seguiría siendo válida.
Cambios en la especificación del lenguaje
Ajustamos la siguiente sección del estándar:
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access
Si la declaración del constructor no tiene inicializador de constructor, la variable
this
se comporta exactamente igual que un parámetroout
del tipo de estructura. En concreto, esto significa que la variable se asignará definitivamente en todas las rutas de acceso de ejecución del constructor de instancia.
Ajustamos este idioma para leer:
Si la declaración del constructor no tiene inicializador de constructor, la variable this
se comporta de forma similar a un parámetro out
del tipo de estructura, excepto que no es un error cuando no se cumplen los requisitos de asignación definitivas (§9.4.1). En su lugar, presentamos los siguientes comportamientos:
- Cuando la variable
this
no cumple los requisitos, todas las variables de instancia sin asignar dentro dethis
en todos los puntos en los que se infringen los requisitos se inicializan implícitamente en el valor predeterminado (§9.3) en una fase de inicialización antes de que se ejecute cualquier otro código del constructor. - Cuando una variable de instancia v dentro de
this
no cumple los requisitos, ni ninguna variable de instancia en ningún nivel de anidamiento dentro de v no cumple los requisitos, v se inicializa implícitamente en el valor predeterminado en una fase de inicialización antes de que se ejecute cualquier otro código del constructor.
Reuniones de diseño
C# feature specifications