Hinweis: Dies scheint behoben worden zu sein Roslyn
Diese Frage stellte sich, als ich meine Antwort auf diese Frage schrieb , in der es um die Assoziativität der Null-Koaleszenz-Operators geht .
Nur zur Erinnerung, die Idee des Null-Koaleszenz-Operators ist, dass ein Ausdruck des Formulars
x ?? y
zuerst bewertet x
, dann:
- Wenn der Wert von
x
null ist,y
wird ausgewertet und das ist das Endergebnis des Ausdrucks - Wenn der Wert von
x
nicht null ist,y
wird er nicht ausgewertet, und der Wert vonx
ist das Endergebnis des Ausdrucks nach einer Konvertierung in den Kompilierungszeittyp von,y
falls erforderlich
Normalerweise ist jetzt keine Konvertierung mehr erforderlich, oder es wird nur von einem nullbaren Typ in einen nicht nullbaren Typ konvertiert - normalerweise sind die Typen gleich oder nur von (sagen wir) int?
nachint
. Sie können jedoch Ihre eigenen impliziten Konvertierungsoperatoren erstellen, die bei Bedarf verwendet werden.
Für den einfachen Fall von x ?? y
ich kein merkwürdiges Verhalten gesehen. Doch mit (x ?? y) ?? z
mir etwas verwirrend Verhalten sehen.
Hier ist ein kurzes, aber vollständiges Testprogramm - die Ergebnisse finden Sie in den Kommentaren:
using System;
public struct A
{
public static implicit operator B(A input)
{
Console.WriteLine("A to B");
return new B();
}
public static implicit operator C(A input)
{
Console.WriteLine("A to C");
return new C();
}
}
public struct B
{
public static implicit operator C(B input)
{
Console.WriteLine("B to C");
return new C();
}
}
public struct C {}
class Test
{
static void Main()
{
A? x = new A();
B? y = new B();
C? z = new C();
C zNotNull = new C();
Console.WriteLine("First case");
// This prints
// A to B
// A to B
// B to C
C? first = (x ?? y) ?? z;
Console.WriteLine("Second case");
// This prints
// A to B
// B to C
var tmp = x ?? y;
C? second = tmp ?? z;
Console.WriteLine("Third case");
// This prints
// A to B
// B to C
C? third = (x ?? y) ?? zNotNull;
}
}
Wir haben also drei benutzerdefinierte Werttypen A
.B
und C
mit Umwandlungen von A nach B, A bis C und B bis C.
Ich kann sowohl den zweiten als auch den dritten Fall verstehen ... aber warum gibt es im ersten Fall eine zusätzliche Umwandlung von A nach B. Insbesondere würde ich wirklich erwartet, dass der erste und der zweite Fall dasselbe sind - es wird schließlich nur ein Ausdruck in eine lokale Variable extrahiert.
Irgendwelche Abnehmer, was los ist? Ich zögere sehr, "Bug" zu schreien, wenn es um den C # -Compiler geht, aber ich bin ratlos, was los ist ...
EDIT: Okay, hier ist ein schlimmeres Beispiel dafür, was los ist, dank der Antwort des Konfigurators, die mir weiteren Grund gibt zu glauben, dass es ein Fehler ist. BEARBEITEN: Das Beispiel benötigt jetzt nicht einmal zwei Null-Koaleszenz-Operatoren ...
using System;
public struct A
{
public static implicit operator int(A input)
{
Console.WriteLine("A to int");
return 10;
}
}
class Test
{
static A? Foo()
{
Console.WriteLine("Foo() called");
return new A();
}
static void Main()
{
int? y = 10;
int? result = Foo() ?? y;
}
}
Die Ausgabe davon ist:
Foo() called
Foo() called
A to int
Die Tatsache, dass Foo()
hier zweimal aufgerufen wird, ist für mich sehr überraschend - ich sehe keinen Grund, den Ausdruck zweimal zu bewerten .
quelle
C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);
. Sie erhalten:Internal Compiler Error: likely culprit is 'CODEGEN'
(("working value" ?? "user default") ?? "system default")
Antworten:
Vielen Dank an alle, die zur Analyse dieses Problems beigetragen haben. Es ist eindeutig ein Compiler-Fehler. Dies scheint nur dann der Fall zu sein, wenn auf der linken Seite des Koaleszenzoperators eine angehobene Konvertierung mit zwei nullbaren Typen erfolgt.
Ich habe noch nicht festgestellt, wo genau etwas schief geht, aber irgendwann während der Kompilierungsphase "Nullable Lowering" - nach der ersten Analyse, aber vor der Codegenerierung - reduzieren wir den Ausdruck
vom obigen Beispiel bis zum moralischen Äquivalent von:
Das ist eindeutig falsch; Das richtige Absenken ist
Aufgrund meiner bisherigen Analyse gehe ich davon aus, dass der nullfähige Optimierer hier aus dem Ruder läuft. Wir haben einen nullbaren Optimierer, der nach Situationen sucht, in denen wir wissen, dass ein bestimmter Ausdruck vom nullbaren Typ möglicherweise nicht null sein kann. Betrachten Sie die folgende naive Analyse: Wir könnten das zuerst sagen
ist das gleiche wie
und dann könnten wir das sagen
ist das gleiche wie
Der Optimierer kann jedoch eingreifen und sagen: "whoa, warte eine Minute, wir haben bereits überprüft, dass temp nicht null ist. Es ist nicht erforderlich, es ein zweites Mal auf null zu überprüfen, nur weil wir einen aufgehobenen Konvertierungsoperator aufrufen." Wir würden sie es einfach optimieren
Ich vermute, dass wir irgendwo die Tatsache zwischenspeichern, dass die optimierte Form von
(int?)Foo()
ist,new int?(op_implicit(Foo().Value))
aber das ist nicht wirklich die optimierte Form, die wir wollen; Wir wollen die optimierte Form von Foo () - ersetzt durch temporär und dann konvertiert.Viele Fehler im C # -Compiler sind auf schlechte Caching-Entscheidungen zurückzuführen. Ein Wort zu den Weisen: Jedes Mal, wenn Sie eine Tatsache zur späteren Verwendung zwischenspeichern, verursachen Sie möglicherweise eine Inkonsistenz, falls sich etwas Relevantes ändert . In diesem Fall hat sich nach der ersten Analyse geändert, dass der Aufruf von Foo () immer als Abruf eines temporären Objekts realisiert werden sollte.
Wir haben den nullbaren Umschreibungsdurchlauf in C # 3.0 viel reorganisiert. Der Fehler wird in C # 3.0 und 4.0 reproduziert, aber nicht in C # 2.0, was bedeutet, dass der Fehler wahrscheinlich mein Fehler war. Es tut uns leid!
Ich werde einen Fehler in die Datenbank eingeben lassen und sehen, ob wir diesen Fehler für eine zukünftige Version der Sprache beheben können. Nochmals vielen Dank an alle für Ihre Analyse; es war sehr hilfreich!
UPDATE: Ich habe den nullbaren Optimierer für Roslyn von Grund auf neu geschrieben. es macht jetzt einen besseren Job und vermeidet diese Art von seltsamen Fehlern. Einige Gedanken zur Funktionsweise des Optimierers in Roslyn finden Sie in meiner Artikelserie, die hier beginnt: https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/
quelle
Dies ist definitiv ein Fehler.
Dieser Code gibt Folgendes aus:
Das ließ mich denken, dass der erste Teil jedes
??
Koaleszenzausdrucks zweimal ausgewertet wird. Dieser Code hat es bewiesen:Ausgänge:
Dies scheint nur zu geschehen, wenn der Ausdruck eine Konvertierung zwischen zwei nullbaren Typen erfordert. Ich habe verschiedene Permutationen ausprobiert, wobei eine der Seiten eine Zeichenfolge ist, und keine davon hat dieses Verhalten verursacht.
quelle
X() ?? Y()
sich intern zu erweiternX() != null ? X() : Y()
, weshalb es zweimal bewertet würde.Wenn Sie sich den generierten Code für den Fall mit der linken Gruppe ansehen, funktioniert er tatsächlich wie folgt (
csc /optimize-
):Ein weiterer Fund, wenn Sie verwenden
first
es wird eine Verknüpfung , wenn beide erzeugta
undb
ist null und Rückkehrc
. Wenn jedocha
oderb
nicht Null ist, wird esa
im Rahmen der impliziten Konvertierung in neu bewertet ,B
bevor zurückgegeben wird, welches vona
oderb
nicht Null ist.Aus der C # 4.0-Spezifikation, §6.1.4:
Dies scheint die zweite Kombination aus Auspacken und Umwickeln zu erklären.
Der C # 2008- und 2010-Compiler erzeugt einen sehr ähnlichen Code. Dies scheint jedoch eine Regression des C # 2005-Compilers (8.00.50727.4927) zu sein, der den folgenden Code für die oben genannten generiert:
Ich frage mich, ob dies nicht auf die zusätzliche Magie zurückzuführen ist , die dem Typinferenzsystem verliehen wird.
quelle
(x ?? y) ?? z
werden verschachtelte Lambdas daraus, was eine ordnungsgemäße Auswertung ohne doppelte Auswertung gewährleistet. Dies ist offensichtlich nicht der Ansatz des C # 4.0-Compilers. Soweit ich das beurteilen kann, wird Abschnitt 6.1.4 in diesem speziellen Codepfad sehr streng angegangen, und die Provisorien werden nicht entfernt, was zu einer doppelten Bewertung führt.Eigentlich werde ich das jetzt einen Fehler nennen, mit dem klareren Beispiel. Dies gilt immer noch, aber die Doppelbewertung ist sicherlich nicht gut.
Es scheint als wäre
A ?? B
implementiert alsA.HasValue ? A : B
. In diesem Fall gibt es auch viel Casting (nach dem regulären Casting für den ternären?:
Operator). Wenn Sie dies alles ignorieren, ist dies sinnvoll, je nachdem, wie es implementiert ist:A ?? B
erweitert sich aufA.HasValue ? A : B
A
ist unserx ?? y
. Erweitern Sie aufx.HasValue : x ? y
(x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B
Hier sehen Sie, dass dies
x.HasValue
zweimal überprüft wird und bei Bedarf zweimalx ?? y
gewirktx
wird.Ich würde es einfach als Artefakt dafür ablegen, wieTake-Away: Erstellen Sie keine impliziten Casting-Operatoren mit Nebenwirkungen.??
Implementierung und nicht als Compiler-Fehler .Es scheint ein Compiler-Fehler zu sein, der sich um die
??
Implementierung dreht . Take-away: Verschachteln Sie verschmelzende Ausdrücke nicht mit Nebenwirkungen.quelle
A() ? A() : B()
das möglicherweiseA()
zweimal ausgewertet wird, aberA() ?? B()
nicht so sehr. Und da es nur beim Casting passiert ... Hmm ... habe ich mich gerade überredet zu denken, dass es sich mit Sicherheit nicht richtig verhält.Ich bin überhaupt kein C # -Experte, wie Sie aus meiner Fragenhistorie ersehen können, aber ich habe es ausprobiert und ich denke, es ist ein Fehler ... aber als Neuling muss ich sagen, dass ich nicht alles verstehe, was vor sich geht hier, damit ich meine Antwort lösche, wenn ich weit weg bin.
Ich bin zu diesem
bug
Schluss gekommen, indem ich eine andere Version Ihres Programms erstellt habe, die sich mit demselben Szenario befasst, aber viel weniger kompliziert ist.Ich verwende drei Ganzzahl-Null-Eigenschaften mit Hintergrundspeichern. Ich setze jedes auf 4 und renne dann
int? something2 = (A ?? B) ?? C;
( Vollständiger Code hier )
Dies liest nur das A und sonst nichts.
Diese Aussage sieht für mich so aus, als ob sie:
Da A also nicht null ist, wird nur A betrachtet und beendet.
In Ihrem Beispiel zeigt das Setzen eines Haltepunkts im ersten Fall, dass x, y und z alle nicht null sind, und daher würde ich erwarten, dass sie genauso behandelt werden wie mein weniger komplexes Beispiel ... aber ich fürchte, ich bin zu viel eines C # Neulings und haben den Punkt dieser Frage völlig verfehlt!
quelle
int
). Er schiebt den Fall weiter in eine dunkle Ecke, indem er mehrere implizite Typkonvertierungen bereitstellt. Dies erfordert den Compiler die sich ändern Art der Daten während der Überprüfung vornull
. Aufgrund dieser impliziten Typkonvertierungen unterscheidet sich sein Beispiel von Ihrem.