Las respuestas de Yuval y David son básicamente correctas; Resumiendo:
- El uso de una variable local no asignada es un error probable, y el compilador puede detectarlo a bajo costo.
- El uso de un campo o elemento de matriz no asignado es menos probable que sea un error, y es más difícil detectar la condición en el compilador. Por lo tanto, el compilador no intenta detectar el uso de una variable no inicializada para los campos y, en cambio, se basa en la inicialización del valor predeterminado para hacer que el comportamiento del programa sea determinista.
Un comentarista de la respuesta de David pregunta por qué es imposible detectar el uso de un campo no asignado a través del análisis estático; Este es el punto que quiero ampliar en esta respuesta.
En primer lugar, para cualquier variable, local o de otro tipo, en la práctica es imposible determinar exactamente si una variable está asignada o no asignada. Considerar:
bool x;
if (M()) x = true;
Console.WriteLine(x);
La pregunta "¿está asignada x?" es equivalente a "¿M () devuelve verdadero?" Ahora, supongamos que M () devuelve verdadero si el último teorema de Fermat es verdadero para todos los enteros de menos de once mil millones, y falso en caso contrario. Para determinar si x está definitivamente asignado, el compilador esencialmente debe producir una prueba del último teorema de Fermat. El compilador no es tan inteligente.
Entonces, lo que hace el compilador para los locales es implementar un algoritmo que es rápido y sobreestima cuando un local no está asignado definitivamente. Es decir, tiene algunos falsos positivos, donde dice "No puedo probar que este local esté asignado" a pesar de que usted y yo sabemos que es así. Por ejemplo:
bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);
Supongamos que N () devuelve un entero. Usted y yo sabemos que N () * 0 será 0, pero el compilador no lo sabe. (Nota: el compilador de C # 2.0 lo sabía, pero eliminé esa optimización, ya que la especificación no dice que el compilador lo sepa).
Muy bien, ¿qué sabemos hasta ahora? No es práctico que los locales obtengan una respuesta exacta, pero podemos sobrestimar la no asignación a bajo costo y obtener un resultado bastante bueno que se equivoque del lado de "hacer que arregle su programa poco claro". Eso es bueno. ¿Por qué no hacer lo mismo para los campos? Es decir, ¿hacer un comprobador de asignación definitiva que sobreestime a bajo costo?
Bueno, ¿de cuántas maneras hay que inicializar un local? Se puede asignar dentro del texto del método. Se puede asignar dentro de una lambda en el texto del método; que lambda nunca se invoque, por lo que esas asignaciones no son relevantes. O se puede pasar como "fuera" a otro método, en cuyo punto podemos suponer que se asigna cuando el método vuelve normalmente. Esos son puntos muy claros en los que se asigna el local, y están allí en el mismo método en que se declara el local . La determinación de la asignación definitiva para locales requiere solo un análisis local . Los métodos tienden a ser cortos, mucho menos de un millón de líneas de código en un método, por lo que analizar todo el método es bastante rápido.
¿Y qué hay de los campos? Los campos se pueden inicializar en un constructor, por supuesto. O un inicializador de campo. O el constructor puede llamar a un método de instancia que inicializa los campos. O el constructor puede llamar a un método virtual que inicializa los campos. O el constructor puede llamar a un método en otra clase , que podría estar en una biblioteca , que inicializa los campos. Los campos estáticos se pueden inicializar en constructores estáticos. Los campos estáticos pueden ser inicializados por otros constructores estáticos.
Esencialmente, el inicializador de un campo podría estar en cualquier parte del programa completo , incluidos los métodos virtuales internos que se declararán en bibliotecas que aún no se han escrito :
// Library written by BarCorp
public abstract class Bar
{
// Derived class is responsible for initializing x.
protected int x;
protected abstract void InitializeX();
public void M()
{
InitializeX();
Console.WriteLine(x);
}
}
¿Es un error compilar esta biblioteca? En caso afirmativo, ¿cómo se supone que BarCorp corrige el error? Al asignar un valor predeterminado a x? Pero eso es lo que el compilador ya hace.
Supongamos que esta biblioteca es legal. Si FooCorp escribe
public class Foo : Bar
{
protected override void InitializeX() { }
}
¿Es eso un error? ¿Cómo se supone que el compilador se da cuenta de eso? La única forma es hacer un análisis completo del programa que rastree la inicialización estática de cada campo en cada ruta posible a través del programa , incluidas las rutas que involucran la elección de métodos virtuales en tiempo de ejecución . Este problema puede ser arbitrariamente difícil ; Puede implicar la ejecución simulada de millones de rutas de control. El análisis de los flujos de control local toma microsegundos y depende del tamaño del método. El análisis de los flujos de control global puede llevar horas porque depende de la complejidad de cada método en el programa y todas las bibliotecas .
Entonces, ¿por qué no hacer un análisis más barato que no tenga que analizar todo el programa y que simplemente se sobreestime aún más severamente? Bueno, proponga un algoritmo que funcione que no dificulte demasiado escribir un programa correcto que realmente compila, y el equipo de diseño puede considerarlo. No conozco tal algoritmo.
Ahora, el comentarista sugiere "requerir que un constructor inicialice todos los campos". Esa no es una mala idea. De hecho, es una idea tan mala que C # ya tiene esa característica para estructuras . Se requiere un constructor de estructura para asignar definitivamente todos los campos para cuando el ctor regrese normalmente; El constructor predeterminado inicializa todos los campos a sus valores predeterminados.
¿Qué hay de las clases? Bueno, ¿cómo sabes que un constructor ha inicializado un campo ? El ctor podría llamar a un método virtual para inicializar los campos, y ahora estamos de vuelta en la misma posición en la que estábamos antes. Las estructuras no tienen clases derivadas; clases de poder. ¿Se requiere una biblioteca que contenga una clase abstracta para contener un constructor que inicialice todos sus campos? ¿Cómo sabe la clase abstracta a qué valores deben inicializarse los campos?
John sugiere simplemente prohibir los métodos de llamada en un ctor antes de que se inicialicen los campos. En resumen, nuestras opciones son:
- Hacer ilegales los modismos de programación comunes, seguros y de uso frecuente.
- Realice un análisis costoso de todo el programa que haga que la compilación tarde horas para buscar errores que probablemente no estén allí.
- Confíe en la inicialización automática a los valores predeterminados.
El equipo de diseño eligió la tercera opción.