usa - tipos de variables en c#
¿Por qué las variables locales requieren inicialización, pero los campos no? (4)
Si creo un bool dentro de mi clase, solo algo como
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), recibo un error "uso de verificación de variable local no asignada".
¿Por qué?
¿Por qué las variables locales requieren inicialización, pero los campos no?
La respuesta breve es que el compilador puede detectar 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 impone 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 lenguaje 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, ya que la
array
se 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 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
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 a
false
cambia algo en esta ruta de ejecución particular?
Probablemente no, considerando que el
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 la "memoria basura", ya que inicializará cualquier valor a su valor predeterminado.
Pero aún así, imagine que este es un tipo de referencia, y que 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 = false
declaraciones
bool b = false
.
Eric Lippert habla de 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 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 (cuando 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 tiene que diseñar, escribir, probar e implementar, y el valor de implementar esto en lugar del esfuerzo realizado sería inútil y complicado.
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; }
}
Property
Boo
puede o
no
haberse inicializado en el constructor.
Entonces, cuando 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
Boo
no 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
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 al 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á definitivamente asignado. Es decir, tiene algunos falsos positivos, donde dice "No puedo probar que este local esté asignado" a pesar de que tú 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 C # 2.0 sí 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 regresa 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 rastrea la inicialización estática de cada campo en cada ruta posible a través del programa , incluidas las rutas que implican 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. Analizar 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 no 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.
- Haga 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.