Roslyn no pudo compilar el código

95

Después de haber migrado mi proyecto de VS2013 a VS2015, el proyecto ya no se compila. Se produce un error de compilación en la siguiente declaración LINQ:

static void Main(string[] args)
{
    decimal a, b;
    IEnumerable<dynamic> array = new string[] { "10", "20", "30" };
    var result = (from v in array
                  where decimal.TryParse(v, out a) && decimal.TryParse("15", out b) && a <= b // Error here
                  orderby decimal.Parse(v)
                  select v).ToArray();
}

El compilador devuelve un error:

Error CS0165 Uso de variable local no asignada 'b'

¿Qué causa este problema? ¿Es posible solucionarlo a través de una configuración de compilador?

ramil89
fuente
11
@BinaryWorrier: ¿Por qué? Solo se usa bdespués de asignarlo mediante un outparámetro.
Jon Skeet
1
La documentación de VS 2015 dice "Aunque las variables que se pasan como argumentos no tienen que inicializarse antes de pasar, se requiere que el método llamado asigne un valor antes de que el método regrese". así que esto parece un error, sí, está garantizado que se inicializará con tryParse.
Rup
3
Independientemente del error, este código ejemplifica todo lo malo de los outargumentos. ¿ TryParseEso devolvería un valor anulable (o equivalente)?
Konrad Rudolph
1
@KonradRudolph se where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= bve mucho mejor
Rawling
2
Solo para tener en cuenta, puede simplificar esto a decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;. He abierto un error Roslyn levantar esto.
Rawling

Respuestas:

112

¿Qué causa este problema?

Me parece un error del compilador. Al menos, lo hizo. Aunque las expresiones decimal.TryParse(v, out a)y decimal.TryParse(v, out b)se evalúan dinámicamente, esperaba que el compilador aún entendiera que para cuando llegue a <= b, ambos ay bdefinitivamente están asignados. Incluso con las rarezas que se pueden encontrar en la escritura dinámica, esperaría evaluar solo a <= bdespués de evaluar ambas TryParsellamadas.

Sin embargo, resulta que a través del operador y la conversión engañosa, es completamente factible tener una expresión A && B && Cque evalúe Ay Cpero no B, si es lo suficientemente astuto. Consulte el informe de errores de Roslyn para ver el ingenioso ejemplo de Neal Gafter.

Hacer que funcione dynamices aún más difícil: la semántica involucrada cuando los operandos son dinámicos es más difícil de describir, porque para realizar una resolución de sobrecarga, es necesario evaluar los operandos para averiguar qué tipos están involucrados, lo que puede ser contrario a la intuición. Sin embargo, de nuevo Neal ha llegado con un ejemplo que demuestra que se requiere que el error del compilador ... esto no es un error, es un error del arreglo . Enormes felicitaciones a Neal por demostrarlo.

¿Es posible solucionarlo a través de la configuración del compilador?

No, pero existen alternativas que evitan el error.

En primer lugar, puede evitar que sea dinámico: si sabe que solo usará cadenas, entonces podría usar IEnumerable<string> o darle a la variable de rango vun tipo de string(es decir from string v in array). Esa sería mi opción preferida.

Si realmente necesita mantenerlo dinámico, simplemente dé bun valor para comenzar con:

decimal a, b = 0m;

Esto no hará ningún daño: sabemos que en realidad su evaluación dinámica no hará nada loco, por lo que aún terminará asignando un valor a bantes de usarlo, haciendo que el valor inicial sea irrelevante.

Además, parece que agregar paréntesis también funciona:

where decimal.TryParse(v, out a) && (decimal.TryParse("15", out b) && a <= b)

Eso cambia el punto en el que se activan varias piezas de resolución de sobrecarga, y hace feliz al compilador.

Hay una cuestión que aún permanecen - reglas de la especificación de la asignación definitiva a la &&necesidad del operador a aclararse a estado que sólo se aplican cuando el &&operador se está utilizando en su aplicación "regular" con dos booloperandos. Intentaré asegurarme de que esto se solucione para el próximo estándar ECMA.

Jon Skeet
fuente
¡Si! Aplicar IEnumerable<string>o agregar corchetes funcionó para mí. Ahora el compilador se compila sin errores.
ramil89
1
usar decimal a, b = 0m;podría eliminar el error, pero luego a <= busaría siempre 0m, ya que el valor de salida aún no se ha calculado.
Paw Baltzersen
12
@PawBaltzersen: ¿Qué te hace pensar eso? Siempre se asignará antes de la comparación, es solo que el compilador no puede probarlo, por alguna razón (un error, básicamente).
Jon Skeet
1
Tener un método de análisis sin efectos secundarios, es decir. decimal? TryParseDecimal(string txt)puede ser una solución también
zahir
1
Me pregunto si es una inicialización perezosa; piensa "si el primero es verdadero, entonces no necesito evaluar el segundo, lo que significa que bpodría no asignarse"; Sé que es un razonamiento inválido pero explica por qué el paréntesis lo corrige ...
durron597
16

Como me educaron tan duro en el informe de errores, intentaré explicarlo yo mismo.


Imagine Tes un tipo definido por el usuario con una conversión implícita boolque alterna entre falsey true, comenzando con false. Por lo que el compilador sabe, el dynamicprimer argumento del primero &&podría evaluar ese tipo, por lo que tiene que ser pesimista.

Si, entonces, deja que el código se compile, esto podría suceder:

  • Cuando el enlazador dinámico evalúa el primero &&, hace lo siguiente:
    • Evaluar el primer argumento
    • Es un T - implícitamente lo envía a bool.
    • Oh, lo es false, así que no necesitamos evaluar el segundo argumento.
    • Haga que el resultado de la &&evaluación sea el primer argumento. (No, no false, por alguna razón).
  • Cuando el enlazador dinámico evalúa el segundo && , hace lo siguiente:
    • Evalúe el primer argumento.
    • Es un T- implícitamente lanzarlo abool.
    • Oh, es true , así que evalúe el segundo argumento.
    • ... Oh mierda, bno está asignado.

En términos de especificaciones, en resumen, existen reglas especiales de "asignación definida" que nos permiten decir no solo si una variable está "definitivamente asignada" o "no definitivamente asignada", sino también si está "definitivamente asignada después de la falseinstrucción" o "definitivamente asignado despuéstrue declaración ".

Estos existen para que al tratar con &&y ||(y !y ??y ?:) el compilador pueda examinar si se pueden asignar variables en ramas particulares de una expresión booleana compleja.

Sin embargo, estos solo funcionan mientras los tipos de expresiones permanecen booleanos . Cuando parte de la expresión es dynamic(o un tipo estático no booleano) ya no podemos decir de manera confiable que la expresión es trueo false; la próxima vez que la usemos boolpara decidir qué rama tomar, es posible que haya cambiado de opinión.


Actualización: esto ahora se ha resuelto y documentado :

Las reglas de asignación definidas implementadas por compiladores anteriores para expresiones dinámicas permitieron algunos casos de código que podrían resultar en la lectura de variables que no están asignadas definitivamente. Consulte https://github.com/dotnet/roslyn/issues/4509 para ver un informe al respecto.

...

Debido a esta posibilidad, el compilador no debe permitir que se compile este programa si val no tiene un valor inicial. Las versiones anteriores del compilador (anteriores a VS2015) permitieron que este programa se compilara incluso si val no tiene un valor inicial. Roslyn ahora diagnostica este intento de leer una variable posiblemente no inicializada.

Rawling
fuente
1
Usando VS2013 en mi otra máquina, de hecho me las arreglé para leer la memoria no asignada usando esto. No es muy emocionante :(
Rawling
Puede leer variables no inicializadas con un simple delegado. Cree un delegado que llegue outa un método que tenga ref. Felizmente lo hará, y hará asignadas variables, sin cambiar el valor.
IllidanS4 quiere a Monica de vuelta el
Por curiosidad, probé ese fragmento con C # v4. Sin embargo, por curiosidad, ¿cómo decide el compilador utilizar el operador false/ trueen lugar del operador de conversión implícito? Localmente, llamará implicit operator boolal primer argumento, luego invocará el segundo operando, llamará operator falseal primer operando, seguido de nuevoimplicit operator bool al primer operando . Esto no tiene sentido para mí, el primer operando debería esencialmente reducirse a un booleano una vez, ¿no?
Rob
@Rob ¿Es este el caso dynamicencadenado &&? Lo he visto básicamente ir (1) evaluar el primer argumento (2) usar la conversión implícita para ver si puedo cortocircuitar (3) No puedo, así que evalúe el segundo argumento (4) ahora conozco ambos tipos, yo Puedo ver que lo mejor &&es un &operador de llamada definido por el usuario (5) falseen el primer argumento para ver si puedo cortocircuitar (6) Puedo (porque falsey implicit boolno estoy de acuerdo), por lo que el resultado es el primer argumento ... y luego el siguiente &&, (7) uso de conversión implícita para ver si puedo cortocircuitar (de nuevo).
Rawling
@ IllidanS4 Suena interesante pero no he descubierto cómo hacerlo. ¿Puedes darme un fragmento?
Rawling
15

Esto no es un error. Consulte https://github.com/dotnet/roslyn/issues/4509#issuecomment-130872713 para ver un ejemplo de cómo una expresión dinámica de este formulario puede dejar una variable sin asignar.

Neal Gafter
fuente
1
Como mi respuesta es aceptada y altamente votada, la he editado para indicar la resolución. Gracias por todo su trabajo en esto, incluida la explicación de mi error :)
Jon Skeet