Por que isso (null ||! TryParse) resulta em “uso de variável local não atribuída”?

98

O código a seguir resulta no uso da variável local não atribuída "numberOfGroups" :

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

No entanto, este código funciona bem (embora ReSharper diga que o = 10é redundante):

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

Estou perdendo algo ou o compilador não está gostando do meu ||?

Eu reduzi isso para dynamiccausar os problemas ( optionsera uma variável dinâmica no meu código acima). A questão ainda permanece, por que não posso fazer isso ?

Este código não 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
}

No entanto, este código faz :

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
}

Eu não sabia que dynamicisso seria um fator.

Brandon Martinez
fonte
Não pense que é inteligente o suficiente para saber que você não está usando o valor passado para seu outparâmetro como entrada
Charleh
3
O código fornecido aqui não demonstra o comportamento descrito; funciona muito bem. Poste um código que realmente demonstre o comportamento que você está descrevendo e que possamos compilar por conta própria. Dê-nos o arquivo completo.
Eric Lippert
8
Ah, agora temos algo interessante!
Eric Lippert
1
Não é muito surpreendente que o compilador esteja confuso com isso. O código auxiliar para o site de chamada dinâmica provavelmente tem algum fluxo de controle que não garante a atribuição ao outparâmetro. Certamente é interessante considerar qual código auxiliar o compilador deve produzir para evitar o problema, ou se isso é possível.
CodesInChaos
1
À primeira vista, com certeza parece um bug.
Eric Lippert

Respostas:

73

Tenho certeza de que é um bug do compilador. Belo achado!

Edit: não é um bug, como demonstra Quartermeister; dynamic pode implementar um trueoperador estranho que pode fazer ycom que nunca seja inicializado.

Aqui está uma reprodução 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; 
    }
}

Não vejo razão para que isso seja ilegal; se você substituir dynamic por bool, ele compilará perfeitamente.

Na verdade, vou me encontrar com a equipe C # amanhã; Vou mencionar isso a eles. Desculpas pelo erro!

Eric Lippert
fonte
6
Estou feliz em saber que não estou enlouquecendo :) Desde então, atualizei meu código para contar apenas com TryParse, então estou pronto por agora. Obrigado pelo seu insight!
Brandon Martinez
4
@NominSim: Suponha que a análise do tempo de execução falhe: então, uma exceção é lançada antes que o local seja lido. Suponha que a análise do tempo de execução seja bem-sucedida: então, no tempo de execução , d é verdadeiro e y é definido, ou d é falso e M define y. De qualquer maneira, y está definido. O fato de a análise ser adiada até o tempo de execução não muda nada.
Eric Lippert
2
Caso alguém esteja curioso: acabei de verificar e o compilador Mono acertou. imgur.com/g47oquT
Dan Tao
17
Acho que o comportamento do compilador está realmente correto, já que o valor de dpode ser de um tipo com um trueoperador sobrecarregado . Publiquei uma resposta com um exemplo em que nenhum dos ramos é usado.
Quartermeister
2
@Quartermeister, caso em que o compilador Mono está errando :)
porges
52

É possível que a variável não seja atribuída se o valor da expressão dinâmica for de um tipo com um operador sobrecarregadotrue .

O ||operador invocará o trueoperador para decidir se avalia o lado direito e, em seguida, a ifinstrução invocará o trueoperador para decidir se avalia seu corpo. Para um normal bool, eles sempre retornarão o mesmo resultado e então exatamente um será avaliado, mas para um operador definido pelo usuário não existe tal garantia!

Construindo a partir da reprodução de Eric Lippert, aqui está um programa curto e completo que demonstra um caso em que nenhum caminho seria executado e a variável teria seu 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
fonte
8
Bom trabalho aqui. Passei isso adiante para as equipes de teste e design do C #; Vou ver se eles têm algum comentário sobre isso quando os vir amanhã.
Eric Lippert
3
Isso é muito estranho para mim. Por que deve dser avaliado duas vezes? (Não estou contestando que seja , como você mostrou.) Eu esperava que o resultado avaliado de true(da primeira chamada do operador, causa por ||) fosse "repassado" para a ifinstrução. Isso certamente aconteceria se você colocasse uma chamada de função lá, por exemplo.
Dan Tao
3
@DanTao: A expressão dé avaliada apenas uma vez, como você espera. É o trueoperador que está sendo chamado duas vezes, uma vez ||e outra vez if.
Quartermeister
2
@DanTao: Pode ficar mais claro se as colocarmos em instruções separadas como var cond = d || M(out y); if (cond) { ... }. Primeiro avaliamos dpara obter uma EvilBoolreferência de objeto. Para avaliar o ||, primeiro invocamos EvilBool.truecom essa referência. Que retorna verdadeiro, por isso curto-circuito e não invocar M, e depois atribuir a referência cond. Em seguida, passamos para a ifdeclaração. A ifinstrução avalia sua condição chamando EvilBool.true.
Quartermeister
2
Agora isso é muito legal. Eu não tinha ideia de que existe um operador verdadeiro ou falso.
IllidanS4 quer Monica de volta
7

Do MSDN (ênfase minha):

O tipo dinâmico permite que as operações em que ele ocorre ignorem a verificação de tipo em tempo de compilação . Em vez disso, essas operações são resolvidas em tempo de execução . O tipo dinâmico simplifica o acesso a APIs COM, como APIs de automação do Office, e também a APIs dinâmicas, como bibliotecas IronPython e ao HTML Document Object Model (DOM).

O tipo dinâmico se comporta como um objeto de texto na maioria das circunstâncias. No entanto, as operações que contêm expressões do tipo dinâmico não são resolvidas ou verificadas pelo compilador.

Visto que o compilador não verifica o tipo ou resolve nenhuma operação que contenha expressões do tipo dinâmico, ele não pode garantir que a variável será atribuída por meio do uso de TryParse().

NominSim
fonte
Se a primeira condição for atendida, numberGroupsé atribuída (no if truebloco), caso contrário, a segunda condição garante a atribuição (via out).
leppie
1
Esse é um pensamento interessante, mas o código compila bem sem o myString == null(contando apenas com o TryParse).
Brandon Martinez
1
@leppie A questão é que, uma vez que a primeira condição (na verdade, portanto, toda a ifexpressão) envolve uma dynamicvariável, ela não é resolvida em tempo de compilação (o compilador, portanto, não pode fazer essas suposições).
NominSim
@NominSim: Entendo seu ponto :) +1 Pode ser um sacrifício do compilador (quebrar as regras do C #), mas outras sugestões parecem implicar em um bug. O trecho de Eric mostra que isso não é um sacrifício, mas um bug.
leppie
@NominSim Isso não pode estar certo; só porque certas funções do compilador são adiadas não significa que todas elas são. Há muitas evidências para mostrar que, em circunstâncias ligeiramente diferentes, o compilador faz a análise de atribuição definitiva sem problemas, apesar da presença de uma expressão dinâmica.
dlev