Schließen Sie die Operatoren kurz || und && existieren für nullfähige Boolesche Werte? Der RuntimeBinder glaubt das manchmal

84

Ich habe die C # -Sprachenspezifikation für die bedingten logischen Operatoren gelesen ||und &&auch als kurzschließende logische Operatoren bezeichnet. Für mich schien es unklar, ob diese für nullfähige Boolesche Werte existieren, dh für den Operandentyp Nullable<bool>(ebenfalls geschrieben bool?), also habe ich es mit nicht dynamischer Typisierung versucht:

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

Das schien die Frage zu klären (ich konnte die Spezifikation nicht klar verstehen, aber vorausgesetzt, die Implementierung des Visual C # -Compilers war korrekt, jetzt wusste ich es).

Ich wollte es aber auch mit dynamicBindung versuchen . Also habe ich stattdessen versucht:

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

Das überraschende Ergebnis ist, dass dies ausnahmslos läuft.

Nun, xund yes ist nicht überraschend, dass ihre Deklarationen dazu führen, dass beide Eigenschaften abgerufen werden und die resultierenden Werte wie erwartet sind, xsind trueund ysind null.

Aber die Auswertung für xxvon A || BBlei zu keiner Bindung einmaligen Ausnahme und nur die Eigenschaft Agelesen wurden, nicht B. Warum passiert das? Wie Sie sehen können, könnten wir den BGetter so ändern , dass er ein verrücktes Objekt zurückgibt "Hello world", und xxwürden es trotzdem trueohne Bindungsprobleme auswerten ...

Das Auswerten A && B(für yy) führt auch zu keinem Bindungszeitfehler. Und hier werden natürlich beide Eigenschaften abgerufen. Warum ist dies durch den Laufzeitbinder zulässig? Wenn das zurückgegebene Objekt von Bin ein "fehlerhaftes" Objekt (wie a string) geändert wird , tritt eine verbindliche Ausnahme auf.

Ist das richtig? (Wie können Sie das aus der Spezifikation ableiten?)

Wenn Sie es Bals ersten Operanden versuchen , geben Sie beide aus B || Aund B && Ageben eine Laufzeitbinder-Ausnahme aus ( B | Aund B & Afunktionieren einwandfrei, da bei nicht kurzgeschlossenen Operatoren |und normal alles normal ist &).

(Versucht mit dem C # -Compiler von Visual Studio 2013 und der Laufzeitversion .NET 4.5.2.)

Jeppe Stig Nielsen
quelle
4
Es gibt überhaupt keine Fälle von Nullable<Boolean>Beteiligung, nur Boxed Booleans, die als behandelt werden dynamic- Ihr Test mit bool?ist irrelevant. (Natürlich ist dies keine vollständige Antwort, nur der Keim von einem.)
Jeroen Mostert
3
Das A || Bmacht insofern einen gewissen Sinn, als Sie nicht bewerten wollen, Bes Asei denn, es ist falsch, was es nicht ist. Sie kennen also nie wirklich die Art des Ausdrucks. Die A && BVersion ist überraschender - ich werde sehen, was ich in der Spezifikation finden kann.
Jon Skeet
2
@JeroenMostert: Nun, es sei denn, der Compiler würde entscheiden, dass ein Operator beteiligt sein könnte , wenn der Typ von Ais boolund der Wert von Bis nullsind bool && bool?.
Jon Skeet
4
Interessanterweise hat dies einen Compiler- oder Spezifikationsfehler aufgedeckt. Die C # 5.0-Spezifikation für &&Gespräche über die Lösung, als ob sie &stattdessen wäre, enthält speziell den Fall, in dem sich beide Operanden befinden bool?- aber der nächste Abschnitt, auf den sie verweist, behandelt den nullbaren Fall nicht. Ich könnte eine Art Antwort hinzufügen, die detaillierter darauf eingeht, aber es würde es nicht vollständig erklären.
Jon Skeet
14
Ich habe Mads eine E-Mail über das Spezifikationsproblem geschickt, um zu sehen, ob es nur ein Problem ist, wie ich es lese ...
Jon Skeet

Antworten:

67

Zunächst einmal vielen Dank, dass Sie darauf hingewiesen haben, dass die Spezifikation für den nicht dynamischen Fall nullable-bool nicht klar ist. Ich werde das in einer zukünftigen Version beheben. Das Verhalten des Compilers ist das beabsichtigte Verhalten. &&und ||sollen nicht auf nullable bools arbeiten.

Der dynamische Binder scheint diese Einschränkung jedoch nicht zu implementieren. Stattdessen werden die Komponentenoperationen separat gebunden: das &/ |und das ?:. Auf diese Weise kann es durcheinander kommen, ob der erste Operand zufällig trueoder ist false(was boolesche Werte sind und somit als erster Operand von zulässig sind ?:), aber wenn Sie nullals ersten Operanden angeben (z. B. wenn Sie es B && Aim obigen Beispiel versuchen ), tun Sie dies Holen Sie sich eine Laufzeitbindungsausnahme.

Wenn Sie darüber nachdenken, können Sie sehen, warum wir dynamisch &&und auf ||diese Weise anstatt als eine große dynamische Operation implementiert haben : Dynamische Operationen werden zur Laufzeit gebunden, nachdem ihre Operanden ausgewertet wurden , sodass die Bindung auf den Laufzeittypen der Ergebnisse basieren kann dieser Bewertungen. Eine solche eifrige Bewertung macht jedoch den Zweck des Kurzschlusses von Betreibern zunichte! Stattdessen generiert der generierte Code für dynamische &&und ||die Auswertung in Teile und wird wie folgt vorgehen:

  • Bewerten Sie den linken Operanden (nennen wir das Ergebnis x)
  • Versuchen Sie, daraus eine boolimplizite Konvertierung oder die Operatoren trueoder zu machen false(fehlgeschlagen, wenn dies nicht möglich ist).
  • Verwenden Sie xals Bedingung in einer ?:Operation
  • Verwenden Sie im wahren Zweig xals Ergebnis
  • Bewerten Sie nun im falschen Zweig den zweiten Operanden (nennen wir das Ergebnis y).
  • Versuchen Sie, den Operator &oder |basierend auf dem Laufzeittyp von xund zu binden y(fehlgeschlagen, wenn dies nicht möglich ist).
  • Wenden Sie den ausgewählten Operator an

Dies ist das Verhalten, das bestimmte "illegale" Kombinationen von Operanden durchlässt: Der ?:Operator behandelt den ersten Operanden erfolgreich als nicht nullbaren Booleschen Wert , der Operator &oder |behandelt ihn erfolgreich als nullbaren Booleschen Wert , und die beiden koordinieren niemals, um zu überprüfen, ob sie übereinstimmen .

Es ist also nicht so dynamisch && und || Arbeit an Nullables. Es ist nur so, dass sie auf eine Weise implementiert werden, die im Vergleich zum statischen Fall etwas zu nachsichtig ist. Dies sollte wahrscheinlich als Fehler angesehen werden, aber wir werden ihn niemals beheben, da dies eine bahnbrechende Änderung wäre. Auch würde es kaum jemandem helfen, das Verhalten zu straffen.

Hoffentlich erklärt dies, was passiert und warum! Dies ist ein faszinierender Bereich, und ich bin oft verblüfft über die Konsequenzen der Entscheidungen, die wir bei der Implementierung von Dynamic getroffen haben. Diese Frage war köstlich - danke, dass Sie sie angesprochen haben!

Mads

Mads Torgersen - MSFT
quelle
Ich kann sehen, dass diese Kurzschlussoperatoren etwas Besonderes sind, da wir bei dynamischer Bindung den Typ des zweiten Operanden in dem Fall, in dem wir kurzschließen, nicht wirklich kennen dürfen. Vielleicht sollte die Spezifikation das erwähnen? Da alles in einem dynamicverpackt ist, können wir natürlich nicht den Unterschied zwischen einem bool?Was HasValueund einem "Einfachen" erkennen bool.
Jeppe Stig Nielsen
6

Ist das richtig?

Ja, ich bin mir ziemlich sicher, dass es so ist.

Wie können Sie das aus der Spezifikation ableiten?

Abschnitt 7.12 von C # Spezifikation Version 5.0, hat Informationen zu den Vergleichsoperator &&und ||und wie dynamische Bindung an sie betrifft. Der relevante Abschnitt:

Wenn ein Operand eines bedingten logischen Operators den Typ der Kompilierungszeit dynamisch hat, ist der Ausdruck dynamisch gebunden (§7.2.2). In diesem Fall ist der Kompilierungszeittyp des Ausdrucks dynamisch, und die unten beschriebene Auflösung erfolgt zur Laufzeit unter Verwendung des Laufzeittyps der Operanden mit dem dynamischen Kompilierungszeittyp.

Dies ist der entscheidende Punkt, der Ihre Frage beantwortet, denke ich. Was ist die Auflösung, die zur Laufzeit passiert? In Abschnitt 7.12.2, Benutzerdefinierte bedingte logische Operatoren wird Folgendes erläutert:

  • Die Operation x && y wird als T.false (x)? x: T. & (x, y), wobei T.false (x) ein Aufruf des in T falsch deklarierten Operators ist und T. & (x, y) ein Aufruf des ausgewählten Operators & ist.
  • Die Operation x || y wird als T.true (x) ausgewertet? x: T. | (x, y), wobei T.true (x) ein Aufruf des in T deklarierten Operators true ist und T. | (x, y) ein Aufruf des ausgewählten Operators | ist.

In beiden Fällen wird der erste Operand x mit den Operatoren falseoder in einen Bool konvertiert true. Dann wird der entsprechende logische Operator aufgerufen. In diesem Sinne verfügen wir über genügend Informationen, um den Rest Ihrer Fragen zu beantworten.

Aber die Auswertung für xx von A || B führte zu keiner Ausnahme für die Bindungszeit, und nur die Eigenschaft A wurde gelesen, nicht B. Warum geschieht dies?

Für den ||Betreiber wissen wir, dass es folgt true(A) ? A : |(A, B). Wir schließen kurz, sodass wir keine verbindliche Zeitausnahme erhalten. Auch wenn Aist false, würden wir noch bekommen nicht eine Laufzeitausnahme zu binden, weil der angegebenen Auflösungsstufe. Wenn dies der Fall Aist false, führen wir den |Operator aus, der erfolgreich Nullwerte verarbeiten kann, gemäß Abschnitt 7.11.4.

Die Auswertung von A & B (für JJ) führt ebenfalls zu keinem Bindungszeitfehler. Und hier werden natürlich beide Eigenschaften abgerufen. Warum ist dies durch den Laufzeitbinder zulässig? Wenn das von B zurückgegebene Objekt in ein "fehlerhaftes" Objekt (wie eine Zeichenfolge) geändert wird, tritt eine Bindungsausnahme auf.

Aus ähnlichen Gründen funktioniert dieser auch. &&wird bewertet als false(x) ? x : &(x, y). Akann erfolgreich in a konvertiert werden bool, daher gibt es dort kein Problem. Da Bnull ist, wird der &Operator (Abschnitt 7.3.7) von dem Operator, der a nimmt, boolzu einem Operator , der die bool?Parameter übernimmt , aufgehoben , und daher gibt es keine Laufzeitausnahme.

Wenn es sich bei beiden bedingten Operatoren Bum etwas anderes als einen Bool (oder eine Nulldynamik) handelt, schlägt die Laufzeitbindung fehl, da keine Überladung gefunden werden kann, die einen Bool und einen Nicht-Bool als Parameter verwendet. Dies geschieht jedoch nur, wenn Adie erste Bedingung für den Bediener ( truefür ||, falsefür &&) nicht erfüllt ist . Der Grund dafür ist, dass die dynamische Bindung ziemlich faul ist. Es wird nicht versucht, den logischen Operator zu binden, es Asei denn, es ist falsch und es muss diesen Pfad gehen, um den logischen Operator auszuwerten. Wenn Adie erste Bedingung für den Bediener nicht erfüllt ist, schlägt sie mit der Bindungsausnahme fehl.

Wenn Sie B als ersten Operanden versuchen, werden beide B || A und B & A geben eine Laufzeitbinder-Ausnahme.

Hoffentlich wissen Sie jetzt bereits, warum dies passiert (oder ich habe es schlecht erklärt). Der erste Schritt beim Auflösen dieses bedingten Operators besteht darin, den ersten Operanden zu verwenden Bund einen der Bool-Konvertierungsoperatoren ( false(B)oder true(B)) zu verwenden, bevor die logische Operation ausgeführt wird. Natürlich Bkann Sein nullnicht in entweder trueoder konvertiert werden false, und daher tritt die Laufzeitbindungsausnahme auf.

Christopher Currens
quelle
Kein Wunder, dass dynamicdie Bindung zur Laufzeit unter Verwendung der tatsächlichen Typen der Instanzen erfolgt, nicht der Typen zur Kompilierungszeit (Ihr erstes Zitat). Ihr zweites Zitat ist irrelevant, da hier kein Typ das operator trueund überlädt operator false. Eine explicit operatorRückkehr boolist etwas anderes als operator trueund false. Es ist schwierig, die Spezifikation so zu lesen, wie es A && B(in meinem Beispiel) erlaubt , ohne auch zuzulassen, a && bwo die aund bstatisch typisierten nullbaren Booleschen Werte sind, dh bool? aund bool? bmit Bindung zur Kompilierungszeit. Dies ist jedoch nicht zulässig.
Jeppe Stig Nielsen
-1

Der Typ Nullable definiert keine bedingten logischen Operatoren || und &&. Ich schlage Ihnen folgenden Code vor:

bool a = true;
bool? b = null;

bool? xxxxOR = (b.HasValue == true) ? (b.Value || a) : a;
bool? xxxxAND = (b.HasValue == true) ? (b.Value && a) : false;
Thomas Papamihos
quelle