¿Por qué las variables locales requieren inicialización, pero los campos no?

140

Si creo un bool dentro de mi clase, solo algo así bool check, el valor predeterminado es falso.

Cuando creo el mismo bool dentro de mi método, bool check(en lugar de dentro de la clase), aparece el error "uso de verificación de variable local no asignada". ¿Por qué?

nachime
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Martijn Pieters
14
La pregunta es vaga. ¿Sería "porque la especificación lo dice" una respuesta aceptable?
Eric Lippert
44
Porque así se hizo en Java cuando lo copiaron. : P
Alvin Thompson

Respuestas:

177

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.

Eric Lippert
fuente
1
Gran respuesta, como siempre. Sin embargo, tengo una pregunta: ¿por qué no asignar automáticamente valores predeterminados a las variables locales también? En otras palabras, ¿por qué no hacer que bool x;sea ​​equivalente bool x = false; incluso dentro de un método ?
durron597
8
@ durron597: Porque la experiencia ha demostrado que olvidarse de asignar un valor a un local probablemente sea un error. Si es probablemente un error y es barato y fácil de detectar, entonces hay un buen incentivo para hacer que el comportamiento sea ilegal o una advertencia.
Eric Lippert
27

Cuando creo el mismo bool dentro de mi método, bool check (en lugar de dentro de la clase), recibo un error "uso de verificación de variable local no asignada". ¿Por qué?

Porque el compilador está tratando de evitar que cometas un error.

¿Inicializar su variable para falsecambiar algo en esta ruta de ejecución particular? Probablemente no, considerarlo default(bool)es falso de todos modos, pero te obliga a ser consciente de que esto está sucediendo. El entorno .NET le impide acceder a "memoria basura", ya que inicializará cualquier valor a su valor predeterminado. Pero aún así, imagine que este es un tipo de referencia, y pasaría un valor no inicializado (nulo) a un método que espera un valor no nulo, y obtendría un NRE en tiempo de ejecución. El compilador simplemente está tratando de evitar eso, aceptando el hecho de que esto a veces puede resultar en bool b = falsedeclaraciones.

Eric Lippert habla sobre esto en una publicación de blog :

La razón por la que queremos que esto sea ilegal no es, como mucha gente cree, porque la variable local se inicializará como basura y queremos protegerlo de la basura. De hecho, inicializamos automáticamente los locales a sus valores predeterminados. (Aunque los lenguajes de programación C y C ++ no lo hacen, y alegremente le permitirán leer basura de un local no inicializado). Más bien, es porque la existencia de una ruta de código de este tipo es probablemente un error, y queremos arrojarle al hoyo de calidad; deberías trabajar duro para escribir ese error.

¿Por qué esto no se aplica a un campo de clase? Bueno, supongo que la línea tuvo que dibujarse en algún lugar, y la inicialización de las variables locales es mucho más fácil de diagnosticar y acertar, en comparación con los campos de clase. El compilador podría hacer esto, pero piense en todas las posibles comprobaciones que necesitaría realizar (donde algunas de ellas son independientes del código de la clase en sí) para evaluar si cada campo de una clase se inicializa. No soy un diseñador de compiladores, pero estoy seguro de que sería definitivamente más difícil, ya que hay muchos casos que se tienen en cuenta y que también deben hacerse de manera oportuna . Para cada característica que tenga que diseñar, escribir, probar e implementar, y el valor de implementar esto en lugar del esfuerzo realizado sería inútil y complicado.

Yuval Itzchakov
fuente
"imagine que este es un tipo de referencia, y pasaría este objeto no inicializado a un método que espera uno inicializado" Quiso decir: "imagine que este era un tipo de referencia y estaba pasando el valor predeterminado (nulo) en lugar de la referencia de un objeto"?
Deduplicador
@Dupuplicator Sí. Un método que espera un valor no nulo. Editado esa parte. Espero que sea más claro ahora.
Yuval Itzchakov
No creo que sea por la línea dibujada. Cada clase supone tener un constructor, al menos el constructor predeterminado. Entonces, cuando se queda con el constructor predeterminado, obtendrá valores predeterminados (silencioso transparente). Al definir un constructor, se espera o se supone que usted sabe lo que está haciendo dentro de él y qué campos desea que se inicialicen de qué manera, incluido el conocimiento de los valores predeterminados.
Peter
Por el contrario: un campo dentro de un método puede ser por valores declarados y asignados a diferentes rutas de ejecución. Puede haber excepciones que son fáciles de supervisar hasta que consulte la documentación de un marco que puede usar o incluso en otras partes del código que no puede mantener. Esto puede introducir una ruta de ejecución muy compleja. Por lo tanto, los compiladores insinúan.
Peter
@Peter Realmente no entendí tu segundo comentario. Con respecto al primero, no hay ningún requisito para inicializar ningún campo dentro de un constructor. Es una práctica común . El trabajo de los compiladores no es imponer esa práctica. No puede confiar en ninguna implementación de un constructor en ejecución y decir "está bien, todos los campos están listos". Eric elaboró ​​mucho en su respuesta sobre las formas en que se puede inicializar un campo de una clase, y muestra cómo tomaría mucho tiempo calcular todas las formas lógicas de inicialización.
Yuval Itzchakov
25

¿Por qué las variables locales requieren inicialización, pero los campos no?

La respuesta corta es que el compilador puede detectar el código de acceso a variables locales no inicializadas de manera confiable, utilizando análisis estático. Mientras que este no es el caso de los campos. Entonces el compilador aplica el primer caso, pero no el segundo.

¿Por qué las variables locales requieren inicialización?

Esto no es más que una decisión de diseño del lenguaje C #, como lo explicó Eric Lippert . El entorno CLR y .NET no lo requieren. VB.NET, por ejemplo, compilará perfectamente con variables locales no inicializadas, y en realidad el CLR inicializa todas las variables no inicializadas a los valores predeterminados.

Lo mismo podría ocurrir con C #, pero los diseñadores de idiomas decidieron no hacerlo. La razón es que las variables inicializadas son una gran fuente de errores y, por lo tanto, al ordenar la inicialización, el compilador ayuda a reducir los errores accidentales.

¿Por qué los campos no requieren inicialización?

Entonces, ¿por qué esta inicialización explícita obligatoria no ocurre con los campos dentro de una clase? Simplemente porque esa inicialización explícita podría ocurrir durante la construcción, a través de una propiedad llamada por un inicializador de objeto, o incluso por un método llamado mucho después del evento. El compilador no puede usar el análisis estático para determinar si cada ruta posible a través del código lleva a que la variable se inicialice explícitamente ante nosotros. Hacerlo mal sería molesto, ya que el desarrollador podría quedarse con un código válido que no se compilará. Por lo tanto, C # no lo impone en absoluto y se deja que CLR inicialice automáticamente los campos a un valor predeterminado si no se establece explícitamente.

¿Qué pasa con los tipos de colección?

La aplicación de C # de la inicialización de variables locales es limitada, lo que a menudo atrapa a los desarrolladores. Considere las siguientes cuatro líneas de código:

string str;
var len1 = str.Length;
var array = new string[10];
var len2 = array[0].Length;

La segunda línea de código no se compilará, ya que está tratando de leer una variable de cadena no inicializada. Sin embargo, la cuarta línea de código se compila muy bien, como arrayse ha inicializado, pero solo con los valores predeterminados. Como el valor predeterminado de una cadena es nulo, obtenemos una excepción en tiempo de ejecución. Cualquiera que haya pasado tiempo aquí en Stack Overflow sabrá que esta inconsistencia de inicialización explícita / implícita conduce a una gran cantidad de "¿Por qué obtengo un error de" Referencia de objeto no establecida en una instancia de un objeto "? preguntas

David Arno
fuente
"El compilador no puede usar el análisis estático para determinar si cada ruta posible a través del código lleva a que la variable se inicialice explícitamente ante nosotros". No estoy convencido de que esto sea cierto. ¿Puedes publicar un ejemplo de un programa que sea resistente al análisis estático?
John Kugelman
@JohnKugelman, considere el caso simple de public interface I1 { string str {get;set;} }y un método int f(I1 value) { return value.str.Length; }. Si esto existe en una biblioteca, el compilador no puede saber a qué se vinculará esa biblioteca, por lo tanto, si setse habrá llamado antes get, el campo de respaldo podría no haberse inicializado explícitamente, pero debe compilar dicho código.
David Arno
Eso es cierto, pero no esperaría que se generara el error durante la compilación f. Se generaría al compilar los constructores. Si deja un constructor con un campo posiblemente sin inicializar, eso sería un error. También podría tener que haber restricciones para llamar a métodos de clase y captadores antes de que se inicialicen todos los campos.
John Kugelman
@JohnKugelman: publicaré una respuesta discutiendo el tema que planteas.
Eric Lippert
44
No es justo. ¡Estamos tratando de tener un desacuerdo aquí!
John Kugelman
10

Buenas respuestas arriba, pero pensé que publicaría una respuesta mucho más simple / más corta para que las personas sean perezosas para leer una larga (como yo).

Clase

class Foo {
    private string Boo;
    public Foo() { /** bla bla bla **/ }
    public string DoSomething() { return Boo; }
}

La propiedad Boopuede o no haberse inicializado en el constructor. Entonces, cuando lo encuentra return Boo;, no asume que se ha inicializado. Simplemente suprime el error.

Función

public string Foo() {
   string Boo;
   return Boo; // triggers error
}

Los { }caracteres definen el alcance de un bloque de código. El compilador recorre las ramas de estos { }bloques haciendo un seguimiento de las cosas. Se puede decir fácilmente que Boono se inicializó. El error entonces se dispara.

¿Por qué existe el error?

El error se introdujo para reducir la cantidad de líneas de código necesarias para que el código fuente sea seguro. Sin el error, lo anterior se vería así.

public string Foo() {
   string Boo;
   /* bla bla bla */
   if(Boo == null) {
      return "";
   }
   return Boo;
}

Del manual:

El compilador de C # no permite el uso de variables no inicializadas. Si el compilador detecta el uso de una variable que podría no haberse inicializado, genera el error del compilador CS0165. Para obtener más información, consulte Campos (Guía de programación de C #). Tenga en cuenta que este error se genera cuando el compilador encuentra una construcción que podría resultar en el uso de una variable no asignada, incluso si su código particular no lo hace. Esto evita la necesidad de reglas demasiado complejas para la asignación definitiva.

Referencia: https://msdn.microsoft.com/en-us/library/4y7h161d.aspx

Reactgular
fuente