Roslyn non è riuscito a compilare il codice


95

Dopo aver migrato il mio progetto da VS2013 a VS2015, il progetto non viene più compilato. Si verifica un errore di compilazione nella seguente istruzione 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();
}

Il compilatore restituisce un errore:

Errore CS0165 Utilizzo della variabile locale non assegnata "b"

Cosa causa questo problema? È possibile risolverlo tramite un'impostazione del compilatore?


11
@BinaryWorrier: perché? Viene utilizzato solo bdopo averlo assegnato tramite un outparametro.
Jon Skeet

1
La documentazione di VS 2015 dice: "Sebbene le variabili passate come argomenti out non debbano essere inizializzate prima di essere passate, il metodo chiamato è necessario per assegnare un valore prima che il metodo ritorni". quindi questo sembra un bug sì, è garantito che venga inizializzato da quel tryParse.
Rup

3
Indipendentemente dall'errore, questo codice esemplificava tutto ciò che è negativo negli outargomenti. Avrebbe TryParserestituito un valore nullable (o equivalente).
Konrad Rudolph

1
@KonradRudolph where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= bsembra molto meglio
Rawling

2
Solo per notare, puoi semplificarlo a decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;. Ho aperto un bug di Roslyn sollevando questo problema .
Rawling

Risposte:


112

Cosa causa questo problema?

A me sembra un bug del compilatore. Almeno, lo ha fatto. Sebbene le espressioni decimal.TryParse(v, out a)e decimal.TryParse(v, out b)vengano valutate dinamicamente, mi aspettavo che il compilatore comprendesse ancora che nel momento in cui raggiunge a <= b, entrambe ae bsono state assegnate definitivamente. Anche con le stranezze che puoi trovare nella digitazione dinamica, mi aspetto di valutare solo a <= bdopo aver valutato entrambe le TryParsechiamate.

Tuttavia, si scopre che attraverso l'operatore e la conversione complicata, è del tutto fattibile avere un'espressione A && B && Cche valuta Ae Cma non B- se sei abbastanza astuto. Vedi il bug report di Roslyn per l'esempio geniale di Neal Gafter.

Fare questo lavoro dynamicè ancora più difficile: la semantica coinvolta quando gli operandi sono dinamici è più difficile da descrivere, perché per eseguire la risoluzione del sovraccarico, è necessario valutare gli operandi per scoprire quali tipi sono coinvolti, il che può essere controintuitivo. Tuttavia, ancora una volta Neal ha escogitato un esempio che mostra che è richiesto l'errore del compilatore ... questo non è un bug, è una correzione di bug . Enormi quantità di complimenti a Neal per averlo dimostrato.

È possibile risolverlo tramite le impostazioni del compilatore?

No, ma ci sono alternative che evitano l'errore.

In primo luogo, potresti impedire che sia dinamico: se sai che utilizzerai solo stringhe, puoi usare IEnumerable<string> o dare alla variabile di intervallo vun tipo di string(cioè from string v in array). Questa sarebbe la mia opzione preferita.

Se hai davvero bisogno di mantenerlo dinamico, dai bun valore per iniziare:

decimal a, b = 0m;

Questo non farà alcun danno: sappiamo che in realtà la tua valutazione dinamica non farà nulla di folle, quindi finirai comunque per assegnare un valore bprima di usarlo, rendendo il valore iniziale irrilevante.

Inoltre, sembra che anche l'aggiunta di parentesi funzioni:

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

Ciò cambia il punto in cui vengono attivati ​​vari pezzi di risoluzione del sovraccarico e sembra rendere felice il compilatore.

Resta ancora un problema: le regole della specifica sull'assegnazione definitiva con l' &&operatore devono essere chiarite per affermare che si applicano solo quando l' &&operatore viene utilizzato nella sua implementazione "regolare" con due booloperandi. Cercherò di assicurarmi che questo sia corretto per il prossimo standard ECMA.


Si! L'applicazione IEnumerable<string>o l'aggiunta di parentesi ha funzionato per me. Ora il compilatore viene compilato senza errori.
ramil89

1
using decimal a, b = 0m;potrebbe rimuovere l'errore, ma poi a <= buserebbe sempre 0m, poiché il valore out non è stato ancora calcolato.
Paw Baltzersen

12
@ PawBaltzersen: cosa te lo fa pensare? E 'sempre sarà assegnato prima del confronto - è solo che il compilatore non può dimostrare che, per qualche ragione (un bug, in fondo).
Jon Skeet

1
Avere un metodo di analisi senza effetti collaterali, ad es. decimal? TryParseDecimal(string txt)potrebbe essere anche una soluzione
zahir

1
Mi chiedo se sia un'inizializzazione pigra; pensa "se il primo è vero non ho bisogno di valutare il secondo che significa che bpotrebbe non essere assegnato"; So che è un ragionamento non valido ma spiega perché le parentesi lo
risolvono


16

Dato che sono stato istruito così duramente nella segnalazione di bug, cercherò di spiegarlo da solo.


Imagine Tè un tipo definito dall'utente con un cast implicito boolche si alterna tra falsee true, a partire da false. Per quanto ne sa il compilatore, il dynamicprimo argomento del primo &&potrebbe restituire quel tipo, quindi deve essere pessimistico.

Se, quindi, lascia compilare il codice, questo potrebbe accadere:

  • Quando il raccoglitore dinamico valuta il primo &&, esegue le seguenti operazioni:
    • Valuta il primo argomento
    • È un T- cast implicitamente a bool.
    • Oh, sì false, quindi non abbiamo bisogno di valutare il secondo argomento.
    • Rendi il risultato della &&valutazione come primo argomento. (No, no false, per qualche motivo.)
  • Quando il raccoglitore dinamico valuta il secondo &&, esegue le seguenti operazioni:
    • Valuta il primo argomento.
    • È un T- cast implicitamente a bool.
    • Oh, è truecosì, quindi valuta il secondo argomento.
    • ... Oh merda, bnon è assegnato.

In termini di specifiche, in breve, ci sono regole speciali di "assegnazione definita" che ci permettono di dire non solo se una variabile è "assegnata definitivamente" o "non assegnata definitivamente", ma anche se è "assegnata definitivamente dopo l' falseistruzione" o "definitivamente assegnato dopo l' trueistruzione ".

Questi esistono in modo che quando si tratta di &&and ||(and !and ??and ?:) il compilatore può esaminare se le variabili possono essere assegnate in particolari rami di un'espressione booleana complessa.

Tuttavia, funzionano solo mentre i tipi delle espressioni rimangono booleani . Quando parte dell'espressione è dynamic(o un tipo statico non booleano) non possiamo più affermare in modo affidabile che l'espressione è trueo false- la prossima volta che la lanciamo boolper decidere quale ramo prendere, potrebbe aver cambiato idea.


Aggiornamento: ora è stato risolto e documentato :

Le regole di assegnazione definita implementate dai precedenti compilatori per le espressioni dinamiche consentivano alcuni casi di codice che potevano portare alla lettura di variabili non assegnate in modo definitivo. Vedi https://github.com/dotnet/roslyn/issues/4509 per un rapporto su questo.

...

A causa di questa possibilità il compilatore non deve consentire la compilazione di questo programma se val non ha un valore iniziale. Le versioni precedenti del compilatore (precedenti a VS2015) consentivano la compilazione di questo programma anche se val non ha un valore iniziale. Roslyn ora diagnostica questo tentativo di leggere una variabile possibilmente non inizializzata.


1
Usando VS2013 sull'altra mia macchina, sono riuscito a leggere la memoria non assegnata usando questo. Non è molto eccitante :(
Rawling

È possibile leggere variabili non inizializzate con semplice delegato. Creare un delegato che acceda outa un metodo che dispone di ref. Lo farà felicemente e assegnerà le variabili, senza cambiare il valore.
IllidanS4 vuole che Monica torni il

Per curiosità, ho testato quello snippet con C # v4. Per curiosità, però, come fa il compilatore a decidere di utilizzare l'operatore false/ truein contrapposizione all'operatore cast implicito? Localmente, chiamerà implicit operator boolsul primo argomento, quindi richiamare il secondo operando, chiamata operator falseal primo operando, seguita dalla implicit operator boolsul primo operando nuovamente . Questo non ha senso per me, il primo operando dovrebbe essenzialmente ridursi a un booleano una volta, no?
Rob

@ Rob è questo il caso dynamicincatenato &&? L'ho visto fondamentalmente andare (1) valutare il primo argomento (2) usare cast implicito per vedere se posso cortocircuitare (3) Non posso, quindi valuta il secondo argomento (4) ora conosco entrambi i tipi, io posso vedere il meglio &&è un &operatore di chiamata (5) definito dall'utente falsesul primo argomento per vedere se posso cortocircuitare (6) Posso (perché falsee implicit boolnon sono d'accordo), quindi il risultato è il primo argomento ... e poi il successivo &&, (7) usa il cast implicito per vedere se riesco a cortocircuitare (di nuovo).
Rawling

@ IllidanS4 Sembra interessante ma non ho scoperto come farlo. Puoi darmi uno snippet?
Rawling

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.