Roslyn falhou ao compilar o código

95

Depois de migrar meu projeto do VS2013 para o VS2015, o projeto não é mais compilado. Um erro de compilação ocorre na seguinte instrução 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();
}

O compilador retorna um erro:

Erro CS0165 Uso de variável local não atribuída 'b'

O que causa esse problema? É possível corrigir isso por meio de uma configuração do compilador?

ramil89
fonte
11
@BinaryWorrier: Por quê? Ele só usa bdepois de atribuí-lo por meio de um outparâmetro.
Jon Skeet
1
A documentação do VS 2015 diz "Embora as variáveis ​​passadas como argumentos externos não precisem ser inicializadas antes de serem passadas, o método chamado é necessário para atribuir um valor antes do retorno do método." então isso parece um bug sim, é garantido que ele seja inicializado por esse tryParse.
Rup
3
Independentemente do erro, esse código exemplifica tudo o que há de ruim nos outargumentos. Isso TryParseretornaria um valor anulável (ou equivalente).
Konrad Rudolph
1
@KonradRudolph where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= bparece muito melhor
Rawling
2
Apenas para observar, você pode simplificar isso para decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;. Eu já abriu um bug Roslyn levantar esta.
Rawling

Respostas:

112

O que causa esse problema?

Parece um bug do compilador para mim. Pelo menos, sim. Embora as expressões decimal.TryParse(v, out a)e decimal.TryParse(v, out b)sejam avaliadas dinamicamente, eu esperava que o compilador ainda entendesse que, no momento em que atinge a <= b, ambos ae bestão definitivamente atribuídos. Mesmo com as estranhezas que você pode descobrir na digitação dinâmica, eu esperaria apenas avaliar a <= bdepois de avaliar ambas as TryParsechamadas.

No entanto, verifica-se que por meio de operador e conversão complicada, é inteiramente viável ter uma expressão A && B && Cque avalia Ae Cmas não B- se você for astuto o suficiente. Veja o relatório de bug de Roslyn para ver o exemplo engenhoso de Neal Gafter.

Fazer isso funcionar dynamicé ainda mais difícil - a semântica envolvida quando os operandos são dinâmicos são mais difíceis de descrever, porque para realizar a resolução de sobrecarga, você precisa avaliar operandos para descobrir quais tipos estão envolvidos, o que pode ser contra-intuitivo. No entanto, novamente Neal veio com um exemplo que mostra que o erro do compilador é necessário ... isso não é um bug, é uma correção de bug . Muitos elogios a Neal por provar isso.

É possível consertá-lo por meio das configurações do compilador?

Não, mas existem alternativas que evitam o erro.

Em primeiro lugar, você pode impedir que seja dinâmico - se você sabe que só usará strings, pode usar IEnumerable<string> ou dar à variável de intervalo vum tipo de string(ou seja from string v in array). Essa seria minha opção preferida.

Se você realmente precisa mantê-lo dinâmico, basta fornecer bum valor para começar:

decimal a, b = 0m;

Isso não fará mal nenhum - sabemos que na verdade sua avaliação dinâmica não fará nada maluco, então você ainda acabará atribuindo um valor a bantes de usá-lo, tornando o valor inicial irrelevante.

Além disso, parece que adicionar parênteses também funciona:

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

Isso muda o ponto em que várias peças de resolução de sobrecarga são acionadas e deixa o compilador feliz.

Ainda existe um problema - as regras da especificação sobre atribuição definitiva com o &&operador precisam ser esclarecidas para afirmar que elas só se aplicam quando o &&operador está sendo usado em sua implementação "regular" com dois booloperandos. Vou tentar garantir que isso seja corrigido para o próximo padrão ECMA.

Jon Skeet
fonte
sim! Aplicar IEnumerable<string>ou adicionar colchetes funcionou para mim. Agora o compilador constrói sem erros.
ramil89 de
1
usando decimal a, b = 0m;pode remover o erro, mas então a <= bseria sempre usar 0m, já que o valor fora não foi calculado ainda.
Paw Baltzersen de
12
@PawBaltzersen: O que o faz pensar isso? Sempre será atribuído antes da comparação - só que o compilador não pode provar isso, por algum motivo (um bug, basicamente).
Jon Skeet de
1
Ter um método de análise sem um efeito colateral, ou seja. decimal? TryParseDecimal(string txt)pode ser uma solução também
zahir
1
Eu me pergunto se é uma inicialização lenta; pensa "se o primeiro for verdadeiro, então não preciso avaliar o segundo, o que significa bque não pode ser atribuído"; Eu sei que esse raciocínio é inválido, mas explica porque os parênteses o corrigem ...
durron597
16

Já que fui educado tanto no relatório de bug, tentarei explicar isso sozinho.


Imagine Té algum tipo definido pelo usuário com uma conversão implícita boolque alterna entre falsee true, começando com false. Até onde o compilador sabe, o dynamicprimeiro argumento para o primeiro &&pode ser avaliado como esse tipo, portanto, ele deve ser pessimista.

Se, então, ele permitir a compilação do código, isso pode acontecer:

  • Quando o fichário dinâmico avalia o primeiro &&, ele faz o seguinte:
    • Avalie o primeiro argumento
    • É um T- implicitamente lançado para bool.
    • Ah, é false, então não precisamos avaliar o segundo argumento.
    • Faça o resultado da &&avaliação como o primeiro argumento. (Não, não false, por algum motivo.)
  • Quando o fichário dinâmico avalia o segundo &&, ele faz o seguinte:
    • Avalie o primeiro argumento.
    • É um T- implicitamente lançado para bool.
    • Oh, é true, então avalie o segundo argumento.
    • ... Oh merda, bnão foi atribuído.

Em termos específicos, em suma, existem regras especiais de "atribuição definida" que nos permitem dizer não apenas se uma variável é "definitivamente atribuída" ou "não definitivamente atribuída", mas também se ela é "definitivamente atribuída após a falsedeclaração" ou "definitivamente atribuído após truedeclaração ".

Eles existem para que, ao lidar com &&e ||(e !e ??e ?:), o compilador possa examinar se as variáveis ​​podem ser atribuídas em ramos específicos de uma expressão booleana complexa.

No entanto, eles só funcionam enquanto os tipos das expressões permanecem booleanos . Quando parte da expressão é dynamic(ou um tipo estático não booleano), não podemos mais dizer com segurança que a expressão é trueou false- na próxima vez em que a lançarmos boolpara decidir qual ramificação tomar, ela pode ter mudado de ideia.


Atualização: isso agora foi resolvido e documentado :

As regras de atribuição definidas implementadas por compiladores anteriores para expressões dinâmicas permitiam alguns casos de código que poderiam resultar na leitura de variáveis ​​que não eram definitivamente atribuídas. Consulte https://github.com/dotnet/roslyn/issues/4509 para obter um relatório sobre isso.

...

Por causa dessa possibilidade, o compilador não deve permitir que este programa seja compilado se val não tiver um valor inicial. As versões anteriores do compilador (anteriores ao VS2015) permitiam que este programa compilasse mesmo se val não tivesse um valor inicial. Roslyn agora diagnostica essa tentativa de ler uma variável possivelmente não inicializada.

Rawling
fonte
1
Usando VS2013 em minha outra máquina, eu realmente consegui ler memória não atribuída usando isso. Não é muito emocionante :(
Rawling
Você pode ler variáveis ​​não inicializadas com delegado simples. Crie um delegado que chegue outa um método que tenha ref. Ele o fará de bom grado e atribuirá variáveis, sem alterar o valor.
IllidanS4 quer Monica de volta em
Por curiosidade, testei esse snippet com C # v4. Porém, por curiosidade - como o compilador decide usar o operador false/ trueem oposição ao operador de conversão implícito? Localmente, ele chamará implicit operator boolno primeiro argumento e, em seguida, no segundo operando, operator falseno primeiro operando e novamenteimplicit operator bool no primeiro operando . Isso não faz sentido para mim, o primeiro operando deve basicamente resumir-se a um booleano uma vez, não?
Rob
@Rob Este é o caso dynamicacorrentado &&? Eu vi basicamente ir (1) avaliar o primeiro argumento (2) usar elenco implícito para ver se consigo curto-circuito (3) Não posso, então analise o segundo argumento (4) agora que conheço os dois tipos, posso ver o melhor &&é um &operador de chamada definido pelo usuário (5) falseno primeiro argumento para ver se consigo curto-circuito (6) posso (porque falsee implicit booldiscordo), então o resultado é o primeiro argumento ... e então o próximo &&, (7) use elenco implícito para ver se consigo curto-circuito (novamente).
Rawling
@ IllidanS4 Parece interessante, mas não descobri como fazer isso. Você pode me dar um trecho?
Rawling
15

Este não é um bug. Consulte https://github.com/dotnet/roslyn/issues/4509#issuecomment-130872713 para um exemplo de como uma expressão dinâmica desta forma pode deixar essa variável de fora sem atribuição.

Neal Gafter
fonte
1
Como minha resposta foi aceita e com muitos votos positivos, eu editei para indicar a resolução. Obrigado por todo o seu trabalho nisso - incluindo a explicação do meu erro para mim :)
Jon Skeet