Perché questo condizionale (null ||! TryParse) comporta "l'uso di una variabile locale non assegnata"?


98

Il codice seguente comporta l' utilizzo della variabile locale non assegnata "numberOfGroups" :

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

Tuttavia, questo codice funziona bene (sebbene, ReSharper dice che = 10è ridondante):

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

Mi manca qualcosa o al compilatore non piace il mio ||?

L'ho ristretto per dynamiccausare i problemi ( optionsera una variabile dinamica nel mio codice sopra). La domanda rimane ancora, perché non posso farlo ?

Questo codice non viene compilato:

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
}

Tuttavia, questo codice fa :

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
}

Non mi rendevo conto che dynamicsarebbe stato un fattore in questo.


Non pensare che sia abbastanza intelligente da sapere che non stai usando il valore passato al tuo outparametro come input
Charleh

3
Il codice qui fornito non dimostra il comportamento descritto; funziona benissimo. Si prega di inserire un codice che dimostri effettivamente il comportamento che stai descrivendo che possiamo compilare da soli. Dacci l'intero file.
Eric Lippert

8
Ah, ora abbiamo qualcosa di interessante!
Eric Lippert

1
Non sorprende che il compilatore sia confuso da questo. Il codice helper per il sito di chiamata dinamico ha probabilmente un flusso di controllo che non garantisce l'assegnazione al outparametro. È certamente interessante considerare quale codice di supporto il compilatore dovrebbe produrre per evitare il problema, o se è anche possibile.
CodesInChaos

1
A prima vista sembra sicuramente un bug.
Eric Lippert

Risposte:


73

Sono abbastanza sicuro che questo sia un bug del compilatore. Bella scoperta!

Modifica: non è un bug, come dimostra Quartermeister; dynamic potrebbe implementare uno strano trueoperatore che potrebbe ynon essere mai inizializzato.

Ecco una riproduzione minima:

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; 
    }
}

Non vedo motivo per cui dovrebbe essere illegale; se sostituisci dynamic con bool, si compila bene.

Domani incontrerò il team C #; Ne parlerò a loro. Mi scuso per l'errore!


6
Sono solo felice di sapere che non sto impazzendo :) Da allora ho aggiornato il mio codice per fare affidamento solo su TryParse, quindi per ora sono pronto. Grazie per la tua intuizione!
Brandon Martinez

4
@NominSim: Supponiamo che l'analisi di runtime fallisca: quindi viene generata un'eccezione prima che il locale venga letto. Supponiamo che l'analisi del runtime abbia esito positivo: in runtime o d è vero e y è impostato, oppure d è falso e M imposta y. In ogni caso, y è impostato. Il fatto che l'analisi venga differita fino al runtime non cambia nulla.
Eric Lippert

2
Nel caso qualcuno sia curioso: ho appena controllato e il compilatore Mono ha capito bene. imgur.com/g47oquT
Dan Tao

17
Penso che il comportamento del compilatore sia effettivamente corretto, poiché il valore di dpuò essere di un tipo con un trueoperatore sovraccarico . Ho pubblicato una risposta con un esempio in cui nessuno dei due rami viene preso.
Quartermeister

2
@Quartermeister, nel qual caso il compilatore Mono sta sbagliando :)
porges

52

È possibile annullare l'assegnazione della variabile se il valore dell'espressione dinamica è di un tipo con un operatore sovraccaricotrue .

L' ||operatore chiamerà l' trueoperatore per decidere se valutare il lato destro, quindi l' ifistruzione chiamerà l' trueoperatore per decidere se valutare il suo corpo. Per un normale bool, questi restituiranno sempre lo stesso risultato e quindi ne verrà valutato esattamente uno, ma per un operatore definito dall'utente non esiste tale garanzia!

Partendo dalla riproduzione di Eric Lippert, ecco un programma breve e completo che mostra un caso in cui nessuno dei percorsi verrebbe eseguito e la variabile avrebbe il suo valore iniziale:

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();
    }
}

8
Bel lavoro qui. L'ho trasmesso ai team di test e progettazione di C #; Quando li vedrò domani vedrò se hanno commenti al riguardo.
Eric Lippert

3
Questo è molto strano per me. Perché dovrebbe dessere valutato due volte? (Non sto contestando che chiaramente è , come hai mostrato.) Mi sarei aspettato il risultato valutato ditrue (dalla prima chiamata dell'operatore, causa con ||) da "passava" per il ifcomunicato. Questo è certamente quello che accadrebbe se mettessi lì una chiamata di funzione, per esempio.
Dan Tao

3
@ DanTao: l'espressione dviene valutata una sola volta, come previsto. È l' trueoperatore che viene invocato due volte, una ||volta e una volta daif .
Quartermeister

2
@ DanTao: potrebbe essere più chiaro se li mettessimo in dichiarazioni separate come var cond = d || M(out y); if (cond) { ... }. Per prima cosa valutiamo dper ottenere un EvilBoolriferimento a un oggetto. Per valutare il ||, invochiamo prima EvilBool.truecon quel riferimento. Ciò restituisce true, quindi cortocircuitiamo e non invochiamo M, quindi assegniamo il riferimento a cond. Quindi, passiamo alla ifdichiarazione. L' ifistruzione valuta la sua condizione chiamando EvilBool.true.
Quartermeister

2
Questo è davvero fantastico. Non avevo idea dell'esistenza di un operatore vero o falso.
IllidanS4 rivuole Monica

7

Da MSDN (enfasi mia):

Il tipo dinamico consente alle operazioni in cui si verifica di ignorare il controllo del tipo in fase di compilazione . Queste operazioni vengono invece risolte in fase di esecuzione . Il tipo dinamico semplifica l'accesso alle API COM come le API di Office Automation e anche alle API dinamiche come le librerie IronPython e al DOM (Document Object Model) HTML.

Il tipo dinamico si comporta come l'oggetto di tipo nella maggior parte delle circostanze. Tuttavia, le operazioni che contengono espressioni di tipo dinamico non vengono risolte o il tipo controllato dal compilatore.

Poiché il compilatore non esegue il controllo del tipo né risolve alcuna operazione che contiene espressioni di tipo dinamico, non può garantire che la variabile verrà assegnata tramite l'uso di TryParse().


Se la prima condizione è soddisfatta, numberGroupsè assegnata (nel if trueblocco), in caso contrario la seconda condizione garantisce l'assegnazione (via out).
leppie

1
Questo è un pensiero interessante, ma il codice si compila bene senza myString == null(basandosi solo su TryParse).
Brandon Martinez

1
@leppie Il punto è che poiché la prima condizione (anzi quindi l'intera ifespressione) coinvolge una dynamicvariabile, non viene risolta in fase di compilazione (il compilatore quindi non può fare quelle ipotesi).
NominSim

@NominSim: vedo il tuo punto :) +1 Potrebbe essere un sacrificio da parte del compilatore (infrangere le regole di C #), ma altri suggerimenti sembrano implicare un bug. Lo snippet di Eric mostra che questo non è un sacrificio, ma un bug.
leppie

@NominSim Questo non può essere giusto; solo perché alcune funzioni del compilatore sono differite non significa che lo siano tutte. Ci sono molte prove per dimostrare che in circostanze leggermente diverse, il compilatore esegue l'analisi definitiva dell'assegnazione senza problemi, nonostante la presenza di un'espressione dinamica.
dlev
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.