¿Por qué este condicional (nulo ||! TryParse) da como resultado un "uso de una variable local no asignada"?

98

El siguiente código da como resultado el uso de la variable local no asignada "numberOfGroups" :

int numberOfGroups;
if(options.NumberOfGroups == null || !int.TryParse(options.NumberOfGroups, out numberOfGroups))
{
    numberOfGroups = 10;
}

Sin embargo, este código funciona bien (aunque ReSharper dice que = 10es redundante):

int numberOfGroups = 10;
if(options.NumberOfGroups == null || !int.TryParse(options.NumberOfGroups, out numberOfGroups))
{
    numberOfGroups = 10;
}

¿Me falta algo o al compilador no le gusta mi ||?

He reducido esto para dynamiccausar los problemas ( optionsera una variable dinámica en mi código anterior). La pregunta sigue siendo, ¿por qué no puedo hacer esto ?

Este código no se compila:

internal class Program
{
    #region Static Methods

    private static void Main(string[] args)
    {
        dynamic myString = args[0];

        int myInt;
        if(myString == null || !int.TryParse(myString, out myInt))
        {
            myInt = 10;
        }

        Console.WriteLine(myInt);
    }

    #endregion
}

Sin embargo, este código :

internal class Program
{
    #region Static Methods

    private static void Main(string[] args)
    {
        var myString = args[0]; // var would be string

        int myInt;
        if(myString == null || !int.TryParse(myString, out myInt))
        {
            myInt = 10;
        }

        Console.WriteLine(myInt);
    }

    #endregion
}

No me di cuenta de dynamicque sería un factor en esto.

Brandon Martinez
fuente
No creo que sea lo suficientemente inteligente como para saber que no está utilizando el valor pasado a su outparámetro como entrada
Charleh
3
El código proporcionado aquí no demuestra el comportamiento descrito; funciona bien. Publique un código que realmente demuestre el comportamiento que está describiendo y que podamos compilar nosotros mismos. Danos el archivo completo.
Eric Lippert
8
¡Ah, ahora tenemos algo interesante!
Eric Lippert
1
No es de extrañar que esto confunda al compilador. El código de ayuda para el sitio de llamada dinámica probablemente tiene algún flujo de control que no garantiza la asignación al outparámetro. Ciertamente es interesante considerar qué código auxiliar debería producir el compilador para evitar el problema, o si eso es posible.
CodesInChaos
1
A primera vista, seguro que parece un error.
Eric Lippert

Respuestas:

73

Estoy bastante seguro de que se trata de un error del compilador. ¡Buen hallazgo!

Editar: no es un error, como demuestra Quartermeister; dynamic podría implementar un trueoperador extraño que podría causar yque nunca se inicialice.

Aquí hay una reproducción mínima:

class Program
{
    static bool M(out int x) 
    { 
        x = 123; 
        return true; 
    }
    static int N(dynamic d)
    {
        int y;
        if(d || M(out y))
            y = 10;
        return y; 
    }
}

No veo ninguna razón por la que eso deba ser ilegal; si reemplaza dynamic con bool, se compila bien.

De hecho, mañana me reuniré con el equipo de C #; Se lo mencionaré. ¡Disculpas por el error!

Eric Lippert
fuente
6
Me alegra saber que no me estoy volviendo loco :) Desde entonces actualicé mi código para depender solo de TryParse, así que estoy listo por ahora. ¡Gracias por tu conocimiento!
Brandon Martinez
4
@NominSim: Supongamos que el análisis en tiempo de ejecución falla: luego se lanza una excepción antes de que se lea el local. Suponga que el análisis en tiempo de ejecución se realiza correctamente: luego, en tiempo de ejecución , d es verdadero y se establece y, od es falso y M establece y. De cualquier manera, se establece y. El hecho de que el análisis se posponga hasta el tiempo de ejecución no cambia nada.
Eric Lippert
2
En caso de que alguien tenga curiosidad: acabo de comprobarlo y el compilador Mono lo hace bien. imgur.com/g47oquT
Dan Tao
17
Creo que el comportamiento del compilador es realmente correcto, ya que el valor de dpuede ser de un tipo con un trueoperador sobrecargado . Publiqué una respuesta con un ejemplo en el que no se toma ninguna rama.
Quartermeister
2
@Quartermeister, en cuyo caso el compilador Mono se equivoca :)
porges
52

Es posible que la variable no esté asignada si el valor de la expresión dinámica es de un tipo con un operador sobrecargadotrue .

El ||operador invocará al trueoperador para decidir si evaluar el lado derecho, y luego la ifdeclaración invocará al trueoperador para decidir si evaluar su cuerpo. Para una normal bool, estos siempre devolverán el mismo resultado y, por lo tanto, se evaluará exactamente uno, ¡pero para un operador definido por el usuario no existe tal garantía!

Partiendo de la reproducción de Eric Lippert, aquí hay un programa corto y completo que demuestra un caso en el que ninguna ruta se ejecutaría y la variable tendría su valor inicial:

using System;

class Program
{
    static bool M(out int x)
    {
        x = 123;
        return true;
    }

    static int N(dynamic d)
    {
        int y = 3;
        if (d || M(out y))
            y = 10;
        return y;
    }

    static void Main(string[] args)
    {
        var result = N(new EvilBool());
        // Prints 3!
        Console.WriteLine(result);
    }
}

class EvilBool
{
    private bool value;

    public static bool operator true(EvilBool b)
    {
        // Return true the first time this is called
        // and false the second time
        b.value = !b.value;
        return b.value;
    }

    public static bool operator false(EvilBool b)
    {
        throw new NotImplementedException();
    }
}
Quartermeister
fuente
8
Buen trabajo aqui. Le he transmitido esto a los equipos de prueba y diseño de C #; Veré si tienen algún comentario al respecto cuando los vea mañana.
Eric Lippert
3
Esto me resulta muy extraño. ¿Por qué debería devaluarse dos veces? (No estoy discutiendo que claramente lo sea , como ha mostrado). Hubiera esperado que el resultado evaluado de true(desde la primera invocación del operador, causa por ||) se "pasara" a la ifdeclaración. Eso es ciertamente lo que sucedería si pones una llamada de función allí, por ejemplo.
Dan Tao
3
@DanTao: la expresión dse evalúa solo una vez, como es de esperar. Es el trueoperador el que se invoca dos veces, una por ||una y otra por if.
Quartermeister
2
@DanTao: Podría ser más claro si los ponemos en declaraciones separadas como var cond = d || M(out y); if (cond) { ... }. Primero evaluamos dpara obtener una EvilBoolreferencia de objeto. Para evaluar el ||, primero invocamos EvilBool.truecon esa referencia. Eso devuelve verdadero, por lo que cortocircuitamos y no invocamos M, y luego asignamos la referencia a cond. Luego, pasamos a la ifdeclaración. La ifdeclaración evalúa su condición llamando EvilBool.true.
Quartermeister
2
Ahora bien, esto es realmente genial. No tenía idea de que existe un operador verdadero o falso.
IllidanS4 quiere que Monica vuelva
7

De MSDN (el énfasis es mío):

El tipo dinámico permite que las operaciones en las que se produce eludan la comprobación de tipos en tiempo de compilación . En cambio, estas operaciones se resuelven en tiempo de ejecución . El tipo dinámico simplifica el acceso a las API COM, como las API de Office Automation, y también a las API dinámicas, como las bibliotecas IronPython, y al modelo de objetos de documento HTML (DOM).

El tipo dinámico se comporta como un objeto de tipo en la mayoría de las circunstancias. Sin embargo, las operaciones que contienen expresiones de tipo dinámico no son resueltas ni verificadas por el compilador.

Dado que el compilador no comprueba el tipo ni resuelve ninguna operación que contenga expresiones de tipo dinámico, no puede asegurar que la variable se asignará mediante el uso de TryParse().

NominSim
fuente
Si se cumple la primera condición, numberGroupsse asigna (en el if truebloque), si no, la segunda condición garantiza la asignación (vía out).
leppie
1
Esa es una idea interesante, pero el código se compila bien sin myString == null(confiando solo en TryParse).
Brandon Martinez
1
@leppie El punto es que dado que la primera condición (de hecho, por lo tanto, la ifexpresión completa ) involucra una dynamicvariable, no se resuelve en el momento de la compilación (el compilador, por lo tanto, no puede hacer esas suposiciones).
NominSim
@NominSim: Veo tu punto :) +1 Podría ser un sacrificio del compilador (rompiendo las reglas de C #), pero otras sugerencias parecen implicar un error. El fragmento de Eric muestra que esto no es un sacrificio, sino un error.
leppie
@NominSim Esto no puede ser correcto; el hecho de que ciertas funciones del compilador estén diferidas no significa que todas lo sean. Hay muchas pruebas que demuestran que, en circunstancias ligeramente diferentes, el compilador realiza el análisis de asignación definitiva sin problemas, a pesar de la presencia de una expresión dinámica.
dlev