C # okay für den Vergleich von Werttypen mit null

85

Ich bin heute darauf gestoßen und habe keine Ahnung, warum der C # -Compiler keinen Fehler auslöst.

Int32 x = 1;
if (x == null)
{
    Console.WriteLine("What the?");
}

Ich bin verwirrt darüber, wie x jemals null sein könnte. Zumal diese Zuweisung definitiv einen Compilerfehler auslöst:

Int32 x = null;

Ist es möglich, dass x null wird, hat Microsoft nur beschlossen, diese Prüfung nicht in den Compiler zu stellen, oder wurde sie vollständig übersehen?

Update: Nachdem der Compiler mit dem Code zum Schreiben dieses Artikels herumgespielt hatte, kam plötzlich eine Warnung heraus, dass der Ausdruck niemals wahr sein würde. Jetzt bin ich wirklich verloren. Ich habe das Objekt in eine Klasse eingefügt und jetzt ist die Warnung verschwunden, aber es bleibt die Frage, ob ein Werttyp am Ende null sein kann.

public class Test
{
    public DateTime ADate = DateTime.Now;

    public Test ()
    {
        Test test = new Test();
        if (test.ADate == null)
        {
            Console.WriteLine("What the?");
        }
    }
}
Joshua Belden
quelle
9
Sie können auch schreiben if (1 == 2). Es ist nicht die Aufgabe des Compilers, eine Codepfadanalyse durchzuführen. Dafür sind statische Analysewerkzeuge und Komponententests gedacht.
Aaronaught
Warum die Warnung wegging, siehe meine Antwort; und nein - es kann keine Null sein.
Marc Gravell
1
Einverstanden mit (1 == 2), wunderte ich mich mehr über die Situation (1 == null)
Joshua Belden
Vielen Dank an alle, die geantwortet haben. Alles macht jetzt Sinn.
Joshua Belden
In Bezug auf die Warnung oder kein Warnproblem: Wenn es sich bei der fraglichen Struktur um einen sogenannten "einfachen Typ" handelt, intgeneriert der Compiler nette Warnungen. Für die einfachen Typen wird der ==Operator durch die C # -Sprachenspezifikation definiert. Bei anderen (nicht einfachen) Strukturen vergisst der Compiler , eine Warnung auszugeben . Weitere Informationen finden Sie unter Falsche Compiler-Warnung beim Vergleich von struct mit null . Bei Strukturen, die keine einfachen Typen sind, ==muss der Operator von einer opeartor ==Methode überladen werden , die Mitglied der Struktur ist (andernfalls ist no ==zulässig).
Jeppe Stig Nielsen

Antworten:

119

Dies ist legal, da die Auflösung von Operatorüberlastungen einen einzigartigen besten Operator zur Auswahl hat. Es gibt einen == -Operator, der zwei nullbare Ints akzeptiert. Das int local kann in ein nullable int konvertiert werden. Das Null-Literal kann in ein null-int konvertiert werden. Daher ist dies eine legale Verwendung des Operators == und führt immer zu false.

In ähnlicher Weise können Sie auch "if (x == 12.6)" sagen, was ebenfalls immer falsch ist. Das int local ist in ein Double konvertierbar, das Literal ist in ein Double konvertierbar, und offensichtlich werden sie niemals gleich sein.

Eric Lippert
quelle
4
Zu Ihrem Kommentar: connect.microsoft.com/VisualStudio/feedback/…
Marc Gravell
5
@James: (Ich ziehe meinen früheren fehlerhaften Kommentar zurück, den ich gelöscht habe.) Benutzerdefinierte Werttypen, für die standardmäßig auch ein benutzerdefinierter Gleichheitsoperator definiert ist, haben einen aufgehobenen benutzerdefinierten Gleichheitsoperator generiert. Der angehobene benutzerdefinierte Gleichheitsoperator ist aus dem von Ihnen angegebenen Grund anwendbar: Alle Werttypen können implizit in ihren entsprechenden nullbaren Typ konvertiert werden, ebenso wie das Nullliteral. Es ist nicht der Fall, dass ein benutzerdefinierter Werttyp, dem ein benutzerdefinierter Vergleichsoperator fehlt, mit dem Nullliteral vergleichbar ist.
Eric Lippert
3
@James: Sicher, Sie können Ihren eigenen Operator == und Operator! = Implementieren, die nullfähige Strukturen annehmen. Wenn diese vorhanden sind, verwendet der Compiler sie, anstatt sie automatisch für Sie zu generieren. (Und im Übrigen bedauere ich, dass die Warnung für den bedeutungslos aufgehobenen Operator auf nicht nullbaren Operanden keine Warnung erzeugt; das ist ein Fehler im Compiler, den wir nicht behoben haben.)
Eric Lippert
2
Wir wollen unsere Warnung! Wir verdienen es.
Jeppe Stig Nielsen
3
@ JamesDunne: Was ist mit dem Definieren static bool operator == (SomeID a, String b)und Markieren eines Obsolete? Wenn der zweite Operand ein untypisiertes Literal nullist, wäre dies eine bessere Übereinstimmung als jede Form, bei der angehobene Operatoren verwendet werden müssen. Wenn SomeID?dies jedoch gleich ist null, würde der angehobene Operator gewinnen.
Supercat
17

Es ist kein Fehler, da es eine ( int?) Konvertierung gibt; Im angegebenen Beispiel wird eine Warnung generiert:

Das Ergebnis des Ausdrucks ist immer 'false', da ein Wert vom Typ 'int' niemals gleich 'null' vom Typ 'int?' Ist.

Wenn Sie die IL überprüfen, werden Sie feststellen, dass der nicht erreichbare Zweig vollständig entfernt wird - er ist in einem Release-Build nicht vorhanden.

Beachten Sie jedoch, dass diese Warnung für benutzerdefinierte Strukturen mit Gleichheitsoperatoren nicht generiert wird. Früher in 2.0, aber nicht im 3.0-Compiler. Der Code wird immer noch entfernt (damit er weiß, dass der Code nicht erreichbar ist), es wird jedoch keine Warnung generiert:

using System;

struct MyValue
{
    private readonly int value;
    public MyValue(int value) { this.value = value; }
    public static bool operator ==(MyValue x, MyValue y) {
        return x.value == y.value;
    }
    public static bool operator !=(MyValue x, MyValue y) {
        return x.value != y.value;
    }
}
class Program
{
    static void Main()
    {
        int i = 1;
        MyValue v = new MyValue(1);
        if (i == null) { Console.WriteLine("a"); } // warning
        if (v == null) { Console.WriteLine("a"); } // no warning
    }
}

Mit dem IL (für Main) - beachten Sie, dass alles außer dem MyValue(1)(was Nebenwirkungen haben könnte) entfernt wurde:

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] int32 i,
        [1] valuetype MyValue v)
    L_0000: ldc.i4.1 
    L_0001: stloc.0 
    L_0002: ldloca.s v
    L_0004: ldc.i4.1 
    L_0005: call instance void MyValue::.ctor(int32)
    L_000a: ret 
}

das ist im Grunde:

private static void Main()
{
    MyValue v = new MyValue(1);
}
Marc Gravell
quelle
1
Jemand hat mir dies kürzlich auch intern gemeldet. Ich weiß nicht, warum wir diese Warnung nicht mehr produzieren. Wir haben es als Fehler eingegeben.
Eric Lippert
1
Los
Marc Gravell
5

Die Tatsache, dass ein Vergleich niemals wahr sein kann, bedeutet nicht, dass er illegal ist. Nein, ein Werttyp kann es jemals sein null.

Adam Robinson
quelle
1
Ein Wertetyp kann jedoch gleich sein null. Überlegen Sie int?, für welchen syntaktischen Zucker es sich um Nullable<Int32>einen Werttyp handelt. Eine Variable vom Typ int?könnte sicherlich gleich sein null.
Greg
1
@ Greg: Ja, es kann gleich null sein, vorausgesetzt, das "gleiche", auf das Sie sich beziehen, ist das Ergebnis des ==Operators. Es ist jedoch wichtig zu beachten, dass die Instanz nicht wirklich null ist.
Adam Robinson
1

Ein Werttyp kann nicht sein null, obwohl er gleich sein kann null(berücksichtigen Nullable<>). In Ihrem Fall werden die intVariablen und nullimplizit umgewandelt Nullable<Int32>und verglichen.

Greg
quelle
0

Ich vermute, dass Ihr bestimmter Test gerade vom Compiler optimiert wird, wenn er die IL generiert, da der Test niemals falsch sein wird.

Randnotiz: Ist es möglich, dass ein nullfähiger Int32 Int32 verwendet? x stattdessen.

GrayWizardx
quelle
0

Ich denke, das liegt daran, dass "==" ein Syntaxzucker ist, der tatsächlich einen Aufruf einer System.Object.EqualsMethode darstellt, die System.ObjectParameter akzeptiert . Null nach ECMA-Spezifikation ist ein spezieller Typ, von dem natürlich abgeleitet wird System.Object.

Deshalb gibt es nur eine Warnung.

Vitaly
quelle
Dies ist aus zwei Gründen nicht korrekt. Erstens hat == nicht die gleiche Semantik wie Object.Equals, wenn eines seiner Argumente ein Referenztyp ist. Zweitens ist null kein Typ. In Abschnitt 7.9.6 der Spezifikation finden Sie Informationen zur Funktionsweise des Referenzgleichheitsoperators.
Eric Lippert
"Das Null-Literal (§9.4.4.6) ergibt den Null-Wert, der verwendet wird, um eine Referenz zu kennzeichnen, die nicht auf ein Objekt oder Array zeigt, oder das Fehlen eines Werts. Der Null-Typ hat einen einzelnen Wert, nämlich die Null Wert. Daher kann ein Ausdruck, dessen Typ der Nulltyp ist, nur den Nullwert auswerten. Es gibt keine Möglichkeit, den Nulltyp explizit zu schreiben, und daher keine Möglichkeit, ihn in einem deklarierten Typ zu verwenden. " - Dies ist ein Zitat von ECMA. Worüber redest du? Welche ECMA-Version verwenden Sie auch? Ich sehe 7.9.6 nicht in meinem.
Vitaly
0

[BEARBEITET: Warnungen in Fehler umgewandelt und Operatoren explizit auf nullable anstatt auf den String-Hack aufmerksam gemacht.]

Gemäß dem cleveren Vorschlag von @ supercat in einem Kommentar oben können Sie mit den folgenden Operatorüberladungen einen Fehler beim Vergleich Ihres benutzerdefinierten Werttyps mit null generieren.

Durch die Implementierung von Operatoren, die mit nullbaren Versionen Ihres Typs verglichen werden, stimmt die Verwendung von null in einem Vergleich mit der nullbaren Version des Operators überein, sodass Sie den Fehler über das Attribut Obsolete generieren können.

Bis Microsoft uns unsere Compiler-Warnung zurückgibt, werde ich diese Problemumgehung durchführen, danke @supercat!

public struct Foo
{
    private readonly int x;
    public Foo(int x)
    {
        this.x = x;
    }

    public override string ToString()
    {
        return string.Format("Foo {{x={0}}}", x);
    }

    public override int GetHashCode()
    {
        return x.GetHashCode();
    }

    public override bool Equals(Object obj)
    {
        return x.Equals(obj);
    }

    public static bool operator ==(Foo a, Foo b)
    {
        return a.x == b.x;
    }

    public static bool operator !=(Foo a, Foo b)
    {
        return a.x != b.x;
    }

    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo a, Foo? b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo a, Foo? b)
    {
        return true;
    }
    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo? a, Foo b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo? a, Foo b)
    {
        return true;
    }
}
yoyo
quelle
Wenn mir nichts fehlt, wird Ihr Ansatz den Compiler zum Kreischen bringen Foo a; Foo? b; ... if (a == b)..., obwohl ein solcher Vergleich absolut legitim sein sollte. Der Grund, warum ich den "String Hack" vorgeschlagen habe, ist, dass er den obigen Vergleich erlauben würde, aber kreischen würde if (a == null). Anstatt zu verwenden string, könnte man jeden anderen Referenztyp als Objectoder ersetzen ValueType; Falls gewünscht, könnte man eine Dummy-Klasse mit einem privaten Konstruktor definieren, der niemals aufgerufen werden könnte, und sie berechtigen ReferenceThatCanOnlyBeNull.
Supercat
Sie sind absolut richtig. Ich hätte klarstellen sollen, dass mein Vorschlag die Verwendung von Nullables unterbricht ... die in der Codebasis, an der ich arbeite, sowieso als sündig angesehen werden (unerwünschtes Boxen usw.). ;)
yoyo
0

Ich denke, die beste Antwort, warum der Compiler dies akzeptiert, ist für generische Klassen. Betrachten Sie die folgende Klasse ...

public class NullTester<T>
{
    public bool IsNull(T value)
    {
        return (value == null);
    }
}

Wenn der Compiler keine Vergleiche nullfür Werttypen akzeptiert , würde er diese Klasse im Wesentlichen unterbrechen, da eine implizite Einschränkung an ihren Typparameter angehängt wird (dh, er würde nur mit nicht wertbasierten Typen funktionieren).

Lee.J.Baxter
quelle
0

Mit dem Compiler können Sie jede Struktur, die das implementiert, ==mit null vergleichen. Sie können sogar ein int mit null vergleichen (Sie würden jedoch eine Warnung erhalten).

Wenn Sie den Code jedoch zerlegen, werden Sie feststellen, dass der Vergleich beim Kompilieren des Codes gelöst wird. So zum Beispiel dieser Code (wo Fooist eine Struktur implementiert ==):

public static void Main()
{
    Console.WriteLine(new Foo() == new Foo());
    Console.WriteLine(new Foo() == null);
    Console.WriteLine(5 == null);
    Console.WriteLine(new Foo() != null);
}

Erzeugt diese IL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       45 (0x2d)
  .maxstack  2
  .locals init ([0] valuetype test3.Program/Foo V_0)
  IL_0000:  nop
  IL_0001:  ldloca.s   V_0
  IL_0003:  initobj    test3.Program/Foo
  IL_0009:  ldloc.0
  IL_000a:  ldloca.s   V_0
  IL_000c:  initobj    test3.Program/Foo
  IL_0012:  ldloc.0
  IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                           valuetype test3.Program/Foo)
  IL_0018:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_001d:  nop
  IL_001e:  ldc.i4.0
  IL_001f:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_0024:  nop
  IL_0025:  ldc.i4.1
  IL_0026:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_002b:  nop
  IL_002c:  ret
} // end of method Program::Main

Wie du siehst:

Console.WriteLine(new Foo() == new Foo());

Übersetzt in:

IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                               valuetype test3.Program/Foo)

Wohingegen:

Console.WriteLine(new Foo() == null);

Wird in false übersetzt:

IL_001e:  ldc.i4.0
hartkodiert
quelle