Eseguire il cortocircuito degli operatori || e && esistono per valori booleani nullable? Il RuntimeBinder a volte la pensa così


84

Ho letto la specifica del linguaggio C # sugli operatori logici condizionali || e &&, noti anche come operatori logici di cortocircuito. A me sembrava poco chiaro se questi esistessero per booleani nullable, ovvero il tipo di operando Nullable<bool>(anche scritto bool?), quindi l'ho provato con la digitazione non dinamica:

bool a = true;
bool? b = null;
bool? xxxx = b || a;  // compile-time error, || can't be applied to these types

Questo sembrava risolvere la questione (non riuscivo a capire chiaramente le specifiche, ma supponendo che l'implementazione del compilatore Visual C # fosse corretta, ora lo sapevo).

Tuttavia, volevo provare anche con l' dynamicassociazione. Quindi ho provato questo invece:

static class Program
{
  static dynamic A
  {
    get
    {
      Console.WriteLine("'A' evaluated");
      return true;
    }
  }
  static dynamic B
  {
    get
    {
      Console.WriteLine("'B' evaluated");
      return null;
    }
  }

  static void Main()
  {
    dynamic x = A | B;
    Console.WriteLine((object)x);
    dynamic y = A & B;
    Console.WriteLine((object)y);

    dynamic xx = A || B;
    Console.WriteLine((object)xx);
    dynamic yy = A && B;
    Console.WriteLine((object)yy);
  }
}

Il risultato sorprendente è che questo funziona senza eccezioni.

Ebbene, xe ynon sorprende, le loro dichiarazioni portano al recupero di entrambe le proprietà e i valori risultanti sono come previsto, xè trueed yè null.

Ma la valutazione di xxof non ha A || Bportato a nessuna eccezione di tempo di associazione, e solo la proprietà è Astata letta, no B. Perché succede questo? Come puoi vedere, potremmo cambiare il Bgetter per restituire un oggetto pazzo, come "Hello world", e xxlo valuteremmo comunque truesenza problemi di binding ...

Anche la valutazione A && B(per yy) non porta a nessun errore di tempo di associazione. E qui vengono recuperate entrambe le proprietà, ovviamente. Perché ciò è consentito dal raccoglitore di runtime? Se l'oggetto restituito da Bviene modificato in un oggetto "cattivo" (come un string), si verifica un'eccezione di associazione.

È questo comportamento corretto? (Come puoi dedurlo dalle specifiche?)

Se provi Bcome primo operando, entrambi B || Ae B && Adanno un'eccezione al binder di runtime ( B | Ae B & Afunzionano bene poiché tutto è normale con operatori non in cortocircuito |e &).

(Provato con il compilatore C # di Visual Studio 2013 e la versione runtime .NET 4.5.2.)


4
Non ci sono istanze di Nullable<Boolean>coinvolto, solo booleani in scatola trattati come dynamic- il tuo test con bool?è irrilevante. (Ovviamente, questa non è una risposta completa, solo il germe di una.)
Jeroen Mostert,

3
La A || Bfa una certa quantità di senso, in quanto non si vuole valutare Bse non Aè falso, che non lo è. Quindi non sai mai il tipo di espressione, davvero. La A && Bversione è più sorprendente: vedrò cosa posso trovare nelle specifiche.
Jon Skeet

2
@ JeroenMostert: Bene, a meno che il compilatore non decida che se il tipo di Aè boole il valore di Bè null, bool && bool?potrebbe essere coinvolto un operatore.
Jon Skeet

4
È interessante notare che sembra che questo abbia esposto un bug del compilatore o delle specifiche. La specifica C # 5.0 per &&parlare di risolverlo come se fosse &invece e include specificamente il caso in cui si trovano entrambi gli operandi bool?, ma poi la sezione successiva a cui fa riferimento non gestisce il caso nullable. Potrei aggiungere una sorta di risposta andando più in dettaglio su questo, ma non lo spiegherebbe completamente.
Jon Skeet

14
Ho inviato un'email a Mads riguardo al problema delle specifiche, per vedere se è solo un problema nel modo in cui lo sto leggendo ...
Jon Skeet

Risposte:


67

Prima di tutto, grazie per aver sottolineato che la specifica non è chiara nel caso nullable-bool non dinamico. Lo risolverò in una versione futura. Il comportamento del compilatore è il comportamento previsto; &&e ||non dovrebbero funzionare su bool nullable.

Tuttavia, il raccoglitore dinamico non sembra implementare questa restrizione. Invece, associa le operazioni del componente separatamente: &/ |e ?:. Quindi è in grado di confondere se il primo operando è trueo false(che sono valori booleani e quindi consentiti come primo operando di ?:), ma se fornisci nullcome primo operando (ad esempio se provi B && Anell'esempio sopra), lo fai ottenere un'eccezione di associazione di runtime.

Se ci pensi, puoi vedere perché abbiamo implementato dinamico &&e in ||questo modo invece che come un'unica grande operazione dinamica: le operazioni dinamiche sono vincolate a runtime dopo che i loro operandi sono stati valutati , in modo che il binding possa essere basato sui tipi di runtime dei risultati di quelle valutazioni. Ma una valutazione così appassionata sconfigge lo scopo di cortocircuitare gli operatori! Quindi, invece, il codice generato per dinamico &&e ||suddivide la valutazione in pezzi e procederà come segue:

  • Valuta l'operando sinistro (chiamiamo il risultato x)
  • Prova a trasformarlo in una boolconversione implicita o gli operatori trueo false(fallisci se non è possibile)
  • Utilizzare xcome condizione in ?:un'operazione
  • Nel vero ramo, usa xcome risultato
  • Nel ramo falso, ora valuta il secondo operando (chiamiamo il risultato y)
  • Prova a associare l' operatore &o in |base al tipo di runtime xe y(fallisci se non è possibile)
  • Applica l'operatore selezionato

Questo è il comportamento che lascia passare alcune combinazioni "illegali" di operandi: l' ?:operatore tratta con successo il primo operando come un booleano non annullabile , l' operatore &o |lo tratta con successo come booleano annullabile , e i due non si coordinano mai per verificare che siano d'accordo .

Quindi non è così dinamico && e || lavorare su nullables. È solo che sono stati implementati in un modo un po 'troppo indulgente, rispetto al caso statico. Questo dovrebbe probabilmente essere considerato un bug, ma non lo risolveremo mai, poiché sarebbe un cambiamento radicale. Inoltre difficilmente aiuterebbe nessuno a rafforzare il comportamento.

Si spera che questo spieghi cosa succede e perché! Questa è un'area intrigante e spesso mi trovo sconcertato dalle conseguenze delle decisioni che abbiamo preso quando abbiamo implementato la dinamica. Questa domanda era deliziosa, grazie per averla sollevata!

Mads


Posso vedere che questi operatori di cortocircuito sono speciali, poiché con l'associazione dinamica non ci è veramente permesso di conoscere il tipo del secondo operando nel caso in cui cortocircuiamo. Forse le specifiche dovrebbero menzionarlo? Naturalmente, poiché tutto all'interno di a dynamicè inscatolato, non possiamo dire la differenza tra a bool?che HasValuee "semplice" bool.
Jeppe Stig Nielsen

6

È questo comportamento corretto?

Sì, sono abbastanza sicuro che lo sia.

Come puoi dedurlo dalle specifiche?

Sezione 7.12 di C # Specification Version 5.0, dispone di informazioni per quanto riguarda gli operatori condizionali &&e ||e come vincolanti dinamica si riferisce a loro. La sezione pertinente:

Se un operando di un operatore logico condizionale ha il tipo in fase di compilazione dinamico, l'espressione è associata dinamicamente (§7.2.2). In questo caso il tipo dell'espressione in fase di compilazione è dinamico e la risoluzione descritta di seguito avverrà in fase di esecuzione utilizzando il tipo in fase di esecuzione di quegli operandi che hanno il tipo in fase di compilazione dinamico.

Questo è il punto chiave che risponde alla tua domanda, credo. Qual è la risoluzione che si verifica in fase di esecuzione? La sezione 7.12.2, Operatori logici condizionali definiti dall'utente spiega:

  • L'operazione x && y viene valutata come T.false (x)? x: T. & (x, y), dove T.false (x) è un'invocazione dell'operatore false dichiarato in T, e T. & (x, y) è un'invocazione dell'operatore selezionato &
  • L'operazione x || y è valutato come T.true (x)? x: T. | (x, y), dove T.true (x) è un'invocazione dell'operatore vero dichiarato in T, e T. | (x, y) è un'invocazione dell'operatore selezionato |.

In entrambi i casi, il primo operando x verrà convertito in un bool utilizzando gli operatori falseo true. Quindi viene chiamato l'operatore logico appropriato. Con questo in mente, abbiamo informazioni sufficienti per rispondere al resto delle tue domande.

Ma la valutazione per xx di A || B non ha portato a nessuna eccezione di tempo di binding e solo la proprietà A è stata letta, non B. Perché questo accade?

Per l' ||operatore sappiamo che segue true(A) ? A : |(A, B). Siamo in corto circuito, quindi non avremo un'eccezione di tempo vincolante. Anche se lo Afosse false, non avremmo comunque un'eccezione di associazione di runtime, a causa dei passaggi di risoluzione specificati. Se Aè false, allora eseguiamo l' |operatore, che può gestire con successo i valori nulli, secondo la Sezione 7.11.4.

Anche la valutazione di A && B (per yy) non comporta alcun errore di binding. E qui vengono recuperate entrambe le proprietà, ovviamente. Perché ciò è consentito dal raccoglitore di runtime? Se l'oggetto restituito da B viene modificato in un oggetto "cattivo" (come una stringa), si verifica un'eccezione di associazione.

Per ragioni simili, funziona anche questo. &&viene valutato come false(x) ? x : &(x, y). Apuò essere convertito con successo in a bool, quindi non ci sono problemi. Poiché Bè nullo, l' &operatore viene spostato (Sezione 7.3.7) da quello che accetta a boola quello che accetta i bool?parametri, e quindi non ci sono eccezioni di runtime.

Per entrambi gli operatori condizionali, se Bè qualcosa di diverso da un bool (o una dinamica null), il binding di runtime non riesce perché non riesce a trovare un overload che accetta un bool e un non bool come parametri. Tuttavia, questo accade solo se Anon riesce a soddisfare il primo condizionale per l'operatore ( truefor ||, falsefor &&). Il motivo per cui ciò accade è perché l'associazione dinamica è piuttosto pigra. Non tenterà di associare l'operatore logico a meno che non Asia falso e deve seguire quel percorso per valutare l'operatore logico. Una volta che Anon riesce a soddisfare la prima condizione per l'operatore, fallirà con l'eccezione vincolante.

Se provi B come primo operando, entrambi B || A e B && A danno un'eccezione di binder di runtime.

Si spera, ormai, che tu sappia già perché questo accade (o ho fatto un pessimo lavoro a spiegare). Il primo passo per risolvere questo operatore condizionale è prendere il primo operando B, e utilizzare uno degli operatori di conversione bool ( false(B)o true(B)) prima di gestire l'operazione logica. Ovviamente, Bbeing nullnon può essere convertito in trueo false, quindi si verifica l'eccezione di binding di runtime.


Non sorprende che con dynamicl'associazione avvenga in fase di esecuzione utilizzando i tipi effettivi delle istanze, non i tipi in fase di compilazione (la prima citazione). La tua seconda citazione è irrilevante poiché nessun tipo qui sovraccarica operator truee operator false. Un explicit operatorritorno boolè qualcosa di diverso da operator truee false. È difficile leggere la specifica in qualsiasi modo che lo consenta A && B(nel mio esempio), senza consentire anche a && bdove ae bsono tipizzati staticamente booleani nullable, cioè bool? ae bool? b, con binding in fase di compilazione. Eppure questo è vietato.
Jeppe Stig Nielsen

-1

Il tipo nullable non definisce gli operatori logici condizionali || e &&. Ti suggerisco di seguire il codice:

bool a = true;
bool? b = null;

bool? xxxxOR = (b.HasValue == true) ? (b.Value || a) : a;
bool? xxxxAND = (b.HasValue == true) ? (b.Value && a) : false;
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.