Was wird bevorzugt: Nullable <T> .HasValue oder Nullable <T>! = Null?

437

Ich habe es immer benutzt, Nullable<>.HasValueweil mir die Semantik gefallen hat. Vor kurzem habe ich jedoch an der vorhandenen Codebasis einer anderen Person gearbeitet, die Nullable<> != nullstattdessen ausschließlich verwendet wurde.

Gibt es einen Grund, einen über den anderen zu verwenden, oder ist es eine reine Präferenz?

  1. int? a;
    if (a.HasValue)
        // ...

vs.

  1. int? b;
    if (b != null)
        // ...
lc.
quelle
9
Ich habe eine ähnliche Frage gestellt ... habe einige gute Antworten erhalten: stackoverflow.com/questions/633286/…
nagelabsen
3
Persönlich würde ich verwenden, HasValueda ich denke, dass Wörter besser lesbar sind als Symbole. Es liegt jedoch ganz bei Ihnen und was zu Ihrem bestehenden Stil passt.
Jake Petroules
1
.HasValueDies ist sinnvoller, da es T?angibt, dass der Typ vom Typ ist und nicht ein Typ, der nullbar sein kann, z. B. Zeichenfolgen.
user3791372

Antworten:

476

Der Compiler ersetzt Nullvergleiche durch einen Aufruf von HasValue, sodass es keinen wirklichen Unterschied gibt. Tun Sie einfach, was für Sie und Ihre Kollegen lesbarer / sinnvoller ist.

Rex M.
quelle
86
Ich würde hinzufügen, "was konsistenter ist / einem bestehenden Codierungsstil folgt".
Josh Lee
20
Beeindruckend. Ich hasse diesen syntaktischen Zucker. int? x = nullgibt mir die Illusion, dass eine nullfähige Instanz ein Referenztyp ist. Die Wahrheit ist jedoch, dass Nullable <T> ein Werttyp ist. Es scheint, als würde ich eine NullReferenceException bekommen : int? x = null; Use(x.HasValue).
KFL
11
@KFL Wenn Sie der syntaktische Zucker stört, verwenden Sie einfach Nullable<int>anstelle von int?.
Cole Johnson
24
In den frühen Phasen der Erstellung einer Anwendung denken Sie möglicherweise, dass es ausreichend ist, einen nullbaren Werttyp zum Speichern einiger Daten zu verwenden, um nach einer Weile zu erkennen, dass Sie eine geeignete Klasse für Ihren Zweck benötigen. Wenn Sie den Originalcode zum Vergleichen mit Null geschrieben haben, haben Sie den Vorteil, dass Sie nicht jeden Aufruf von HasValue () durch einen Nullvergleich suchen / ersetzen müssen.
Anders
14
Es ist ziemlich dumm, sich darüber zu beschweren, dass man Nullable auf Null setzen oder mit Null vergleichen kann, wenn man das Nullable nennt . Das Problem ist, dass Leute "Referenztyp" mit "kann null sein" in Konflikt bringen, aber das ist eine konzeptionelle Verwirrung. Zukünftiges C # wird nicht nullbare Referenztypen haben.
Jim Balter
48

Ich bevorzuge (a != null)es, dass die Syntax mit Referenztypen übereinstimmt.

cbp
quelle
11
Das ist natürlich ziemlich irreführend, da Nullable<>es sich nicht um einen Referenztyp handelt.
Luaan
9
Ja, aber die Tatsache spielt an dem Punkt, an dem Sie keine Prüfung durchführen, normalerweise nur eine sehr geringe Rolle.
cbp
31
Es ist nur irreführend für die konzeptionell verwirrte. Die Verwendung einer konsistenten Syntax für zwei verschiedene Typen bedeutet nicht, dass sie vom selben Typ sind. C # hat nullbare Referenztypen (alle Referenztypen sind derzeit nullbar, dies wird sich jedoch in Zukunft ändern) und nullbare Werttypen. Die Verwendung einer konsistenten Syntax für alle nullbaren Typen ist sinnvoll. In keiner Weise bedeutet dies, dass nullbare Werttypen Referenztypen sind oder dass nullbare Referenztypen Werttypen sind.
Jim Balter
Ich bevorzuge, HasValueweil es besser lesbar ist als!= null
Konrad
Die Codierungskonsistenz ist besser lesbar, wenn Sie nicht verschiedene Schreibstile für denselben Code mischen. Da nicht alle Orte eine .HasValue-Eigenschaft haben, wird seitdem! = Null verwendet, um die Konsistenz zu erhöhen. Meiner Meinung nach.
ColacX
21

Ich habe einige Nachforschungen angestellt, indem ich verschiedene Methoden verwendet habe, um einem nullbaren int Werte zuzuweisen. Folgendes ist passiert, als ich verschiedene Dinge getan habe. Sollte klären, was los ist. Denken Sie daran: Nullable<something>oder die Kurzschrift something?ist eine Struktur, für die der Compiler anscheinend viel Arbeit leistet, damit wir sie mit null verwenden können, als wäre es eine Klasse.
Wie Sie unten sehen werden, SomeNullable == nullund SomeNullable.HasValuewird immer wieder zurückkehren ein wahr oder falsch erwartet. Obwohl unten nicht gezeigt, SomeNullable == 3ist es auch gültig (vorausgesetzt, SomeNullable ist ein int?).
Während SomeNullable.Valueuns einen Laufzeitfehler bekommt , wenn wir zugewiesen nullzu SomeNullable. Dies ist in der Tat der einzige Fall, in dem Nullables dank einer Kombination überladener Operatoren, die überladen sind, ein Problem verursachen könnenobject.Equals(obj) Methode und Compiler-Optimierung und Affengeschäft.

Hier ist eine Beschreibung von Code, den ich ausgeführt habe, und welche Ausgabe er in Labels erzeugt hat:

int? val = null;
lbl_Val.Text = val.ToString(); //Produced an empty string.
lbl_ValVal.Text = val.Value.ToString(); //Produced a runtime error. ("Nullable object must have a value.")
lbl_ValEqNull.Text = (val == null).ToString(); //Produced "True" (without the quotes)
lbl_ValNEqNull.Text = (val != null).ToString(); //Produced "False"
lbl_ValHasVal.Text = val.HasValue.ToString(); //Produced "False"
lbl_NValHasVal.Text = (!(val.HasValue)).ToString(); //Produced "True"
lbl_ValValEqNull.Text = (val.Value == null).ToString(); //Produced a runtime error. ("Nullable object must have a value.")
lbl_ValValNEqNull.Text = (val.Value != null).ToString(); //Produced a runtime error. ("Nullable object must have a value.")

Ok, versuchen wir die nächste Initialisierungsmethode:

int? val = new int?();
lbl_Val.Text = val.ToString(); //Produced an empty string.
lbl_ValVal.Text = val.Value.ToString(); //Produced a runtime error. ("Nullable object must have a value.")
lbl_ValEqNull.Text = (val == null).ToString(); //Produced "True" (without the quotes)
lbl_ValNEqNull.Text = (val != null).ToString(); //Produced "False"
lbl_ValHasVal.Text = val.HasValue.ToString(); //Produced "False"
lbl_NValHasVal.Text = (!(val.HasValue)).ToString(); //Produced "True"
lbl_ValValEqNull.Text = (val.Value == null).ToString(); //Produced a runtime error. ("Nullable object must have a value.")
lbl_ValValNEqNull.Text = (val.Value != null).ToString(); //Produced a runtime error. ("Nullable object must have a value.")

Trotzdem wie zuvor. int? val = new int?(null);Beachten Sie, dass das Initialisieren mit , wobei null an den Konstruktor übergeben wurde, einen COMPILE-Zeitfehler erzeugt hätte, da der Wert des nullbaren Objekts NICHT nullwertfähig ist. Nur das Wrapper-Objekt selbst kann gleich null sein.

Ebenso würden wir einen Kompilierungszeitfehler erhalten von:

int? val = new int?();
val.Value = null;

ganz zu schweigen davon, dass dies val.Valuesowieso eine schreibgeschützte Eigenschaft ist, was bedeutet, dass wir nicht einmal so etwas verwenden können wie:

val.Value = 3;

Aber auch hier können wir mit polymorphen überladenen impliziten Konvertierungsoperatoren Folgendes tun:

val = 3;

Sie müssen sich keine Sorgen um Polysomthing machen, was auch immer funktioniert, solange es richtig funktioniert? :) :)

Perrin Larson
quelle
5
"Denken Sie daran: Nullable <something> oder die Abkürzung etwas? Ist eine Klasse." Das ist falsch! Nullable <T> ist eine Struktur. Der Operator Equals und == wird überladen, um im Vergleich zu null true zurückzugeben. Der Compiler macht für diesen Vergleich keine ausgefallene Arbeit.
Andrewjs
1
@andrewjs - Sie haben Recht, dass es sich um eine Struktur handelt (keine Klasse), aber Sie haben Unrecht, dass sie den Operator == überlastet. Wenn Sie Nullable<X>VisualStudio 2013 und F12 eingeben, werden Sie feststellen, dass nur die Konvertierung von und nach Xund die Equals(object other)Methode überlastet werden. Ich denke jedoch, dass der Operator == diese Methode standardmäßig verwendet, sodass der Effekt der gleiche ist. Eigentlich wollte ich diese Antwort schon seit einiger Zeit aktualisieren, aber ich bin faul und / oder beschäftigt. Dieser Kommentar muss erstmal gemacht werden :)
Perrin Larson
Ich habe ildasm kurz durchgesehen und Sie haben Recht damit, dass der Compiler etwas Magie macht. Das Vergleichen eines Nullable <T> -Objekts mit Null führt tatsächlich zu einem Aufruf von HasValue. Interessant!
Andrewjs
3
@andrewjs Eigentlich macht der Compiler eine Menge Arbeit, um Nullables zu optimieren. Wenn Sie beispielsweise einem nullbaren Typ einen Wert zuweisen, ist dieser überhaupt nicht nullwertfähig (z int? val = 42; val.GetType() == typeof(int). B. ). Nullable ist also nicht nur eine Struktur, die gleich null sein kann, sondern oft auch überhaupt nicht nullable! : D Auf die gleiche Weise boxen Sie, wenn Sie einen nullbaren Wert boxen int, nicht int?- und wenn der int?keinen Wert hat, erhalten Sie nullanstelle eines nullbaren Boxwerts. Es bedeutet im Grunde, dass es selten Overhead gibt,
wenn
1
@ JimBalter Wirklich? Das ist sehr interessant. Was sagt Ihnen der Speicherprofiler über ein nullbares Feld in einer Klasse? Wie deklarieren Sie einen Werttyp, der von einem anderen Werttyp in C # erbt? Wie deklarieren Sie Ihren eigenen nullbaren Typ, der sich genauso verhält wie der nullbare Typ von .NET? Seit wann ist Nullein Typ in .NET? Können Sie auf den Teil in der CLR / C # -Spezifikation verweisen, in dem dies gesagt wird? Nullables sind in der CLR-Spezifikation gut definiert, ihr Verhalten ist keine "Implementierung einer Abstraktion" - es ist ein Vertrag . Aber wenn das Beste, was Sie tun können, Ad-Hominem-Angriffe sind, amüsieren Sie sich.
Luaan
13

In VB.Net. Verwenden Sie NICHT "IsNot Nothing", wenn Sie ".HasValue" verwenden können. Ich habe gerade einen mittleren Vertrauensfehler "Operation könnte die Laufzeit destabilisieren" gelöst, indem ich "IsNot Nothing" an einer Stelle durch ".HasValue" ersetzt habe. Ich verstehe nicht wirklich warum, aber im Compiler passiert etwas anders. Ich würde annehmen, dass "! = Null" in C # das gleiche Problem haben könnte.

Carter Medlin
quelle
8
Ich würde HasValuewegen der Lesbarkeit bevorzugen . IsNot Nothingist wirklich ein hässlicher Ausdruck (wegen der doppelten Verneinung).
Stefan Steinegger
12
@steffan "IsNot Nothing" ist keine doppelte Verneinung. "Nichts" ist kein Negativ, es ist eine diskrete Größe, auch außerhalb des Bereichs der Programmierung. "Diese Menge ist nicht nichts." ist grammatikalisch genau das gleiche wie "Diese Menge ist nicht Null". und weder ist ein doppeltes Negativ.
jmbpiano
5
Es ist nicht so, dass ich der Abwesenheit der Wahrheit hier nicht widersprechen möchte, aber komm jetzt. IsNot Nothing ist eindeutig zu negativ. Warum nicht etwas Positives und Klares wie HasValue schreiben? Dies ist kein Grammatiktest, sondern eine Codierung, bei der das Hauptziel die Klarheit ist.
Randy Gamage
3
jmbpiano: Ich stimme zu, es ist keine doppelte Negation, aber es ist eine einzelne Negation und das ist fast so hässlich und nicht so klar wie ein einfacher positiver Ausdruck.
Kaveh Hadjari
0

Wenn Sie linq verwenden und Ihren Code kurz halten möchten, empfehle ich, immer zu verwenden !=null

Und deshalb:

Lassen Sie sich vorstellen , wir eine Klasse Foomit einer Nullable - Doppel VariableSomeDouble

public class Foo
{
    public double? SomeDouble;
    //some other properties
}   

Wenn wir irgendwo in unserem Code alle Foo mit einem SomeDouble-Wert ungleich Null aus einer Sammlung von Foo erhalten möchten (vorausgesetzt, einige Foos in der Sammlung können auch null sein), haben wir am Ende mindestens drei Möglichkeiten, unsere Funktion zu schreiben (wenn wir benutze C # 6):

public IEnumerable<Foo> GetNonNullFoosWithSomeDoubleValues(IEnumerable<Foo> foos)
{
     return foos.Where(foo => foo?.SomeDouble != null);
     return foos.Where(foo=>foo?.SomeDouble.HasValue); // compile time error
     return foos.Where(foo=>foo?.SomeDouble.HasValue == true); 
     return foos.Where(foo=>foo != null && foo.SomeDouble.HasValue); //if we don't use C#6
}

Und in solchen Situationen empfehle ich, immer die kürzere zu wählen

Yan Yankelevich
quelle
2
Ja, foo?.SomeDouble.HasValueist in diesem Kontext ein Fehler zur Kompilierungszeit (kein "Wurf" in meiner Terminologie), da sein Typ bool?nicht nur ist bool. (Die .WhereMethode will a Func<Foo, bool>.) Das ist (foo?.SomeDouble).HasValuenatürlich erlaubt , da das Typ hat bool. Dies ist, was Ihre erste Zeile vom C # -Compiler (zumindest formal) intern "übersetzt" wird.
Jeppe Stig Nielsen
-6

Allgemeine Antwort und Faustregel: Wenn Sie die Möglichkeit haben (z. B. benutzerdefinierte Serializer zu schreiben), Nullable in einer anderen Pipeline zu verarbeiten als object- und ihre spezifischen Eigenschaften zu verwenden -, tun Sie dies und verwenden Sie Nullable-spezifische Eigenschaften. Aus konsequenter Sicht HasValuesollte daher bevorzugt werden. Konsequentes Denken kann Ihnen helfen, besseren Code zu schreiben, ohne zu viel Zeit mit Details zu verbringen. Zum Beispiel wird die zweite Methode um ein Vielfaches effektiver sein (hauptsächlich aufgrund von Inlinern und Boxen von Compilern, aber dennoch sind Zahlen sehr aussagekräftig):

public static bool CheckObjectImpl(object o)
{
    return o != null;
}

public static bool CheckNullableImpl<T>(T? o) where T: struct
{
    return o.HasValue;
}

Benchmark-Test:

BenchmarkDotNet=v0.10.5, OS=Windows 10.0.14393
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233539 Hz, Resolution=309.2587 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0
  Core   : .NET Core 4.6.25009.03, 64bit RyuJIT


        Method |  Job | Runtime |       Mean |     Error |    StdDev |        Min |        Max |     Median | Rank |  Gen 0 | Allocated |
-------------- |----- |-------- |-----------:|----------:|----------:|-----------:|-----------:|-----------:|-----:|-------:|----------:|
   CheckObject |  Clr |     Clr | 80.6416 ns | 1.1983 ns | 1.0622 ns | 79.5528 ns | 83.0417 ns | 80.1797 ns |    3 | 0.0060 |      24 B |
 CheckNullable |  Clr |     Clr |  0.0029 ns | 0.0088 ns | 0.0082 ns |  0.0000 ns |  0.0315 ns |  0.0000 ns |    1 |      - |       0 B |
   CheckObject | Core |    Core | 77.2614 ns | 0.5703 ns | 0.4763 ns | 76.4205 ns | 77.9400 ns | 77.3586 ns |    2 | 0.0060 |      24 B |
 CheckNullable | Core |    Core |  0.0007 ns | 0.0021 ns | 0.0016 ns |  0.0000 ns |  0.0054 ns |  0.0000 ns |    1 |      - |       0 B |

Benchmark-Code:

public class BenchmarkNullableCheck
{
    static int? x = (new Random()).Next();

    public static bool CheckObjectImpl(object o)
    {
        return o != null;
    }

    public static bool CheckNullableImpl<T>(T? o) where T: struct
    {
        return o.HasValue;
    }

    [Benchmark]
    public bool CheckObject()
    {
        return CheckObjectImpl(x);
    }

    [Benchmark]
    public bool CheckNullable()
    {
        return CheckNullableImpl(x);
    }
}

https://github.com/dotnet/BenchmarkDotNet wurde verwendet

PS . Die Leute sagen, dass Ratschläge "HasValue wegen konsequentem Denken bevorzugen" nicht verwandt und nutzlos sind. Können Sie die Leistung davon vorhersagen?

public static bool CheckNullableGenericImpl<T>(T? t) where T: struct
{
    return t != null; // or t.HasValue?
}

PPS- Leute fahren mit Minus fort, aber niemand versucht, die Leistung von vorherzusagen CheckNullableGenericImpl. Und dort hilft Ihnen der Compiler nicht beim Ersetzen !=nulldurch HasValue. HasValuesollte direkt verwendet werden, wenn Sie an Leistung interessiert sind.

Roman Pokrovskij
quelle
2
Ihre CheckObjectImpl Boxen sind nullbar in eine object, während CheckNullableImplBoxen nicht verwendet wird. Daher ist der Vergleich sehr unfehlbar. Nicht nur ist es nicht Kost, sondern auch nutzlos , weil, wie in der erwähnten akzeptierten Antwort , die Compiler Schreibungen !=auf HasValuejeden Fall.
GSerg
2
Die Leser ignorieren nicht die Struktur von Nullable<T>( Sie boxen es in eine object). Wenn Sie != nullmit einer Nullable auf der linken Seite anwenden , tritt kein Boxing auf, da die Unterstützung !=für Nullables auf Compilerebene funktioniert. Es ist anders, wenn Sie die Nullable vor dem Compiler verbergen, indem Sie sie zuerst in eine Box einfügen object. Weder CheckObjectImpl(object o)Ihr Benchmark noch Ihr Benchmark sind grundsätzlich sinnvoll.
GSerg
3
Mein Problem ist, dass mir die Qualität der Inhalte auf dieser Website am Herzen liegt. Was Sie gepostet haben, ist entweder irreführend oder falsch. Wenn Sie versucht haben, die Frage des OP zu beantworten, ist Ihre Antwort völlig falsch. Dies lässt sich leicht beweisen, indem Sie den Anruf an CheckObjectImpldurch seinen Körper im Inneren ersetzen CheckObject. Ihre jüngsten Kommentare zeigen jedoch, dass Sie tatsächlich eine völlig andere Frage im Sinn hatten, als Sie sich entschieden haben, diese 8 Jahre alte Frage zu beantworten, was Ihre Antwort im Kontext der ursprünglichen Frage irreführend macht. Es war nicht das, wonach das OP fragte.
GSerg
3
Versetzen Sie sich in die Lage des nächsten Mannes, der googelt what is faster != or HasValue. Er kommt zu dieser Frage, blättert in Ihrer Antwort, schätzt Ihren Benchmark und sagt: "Gee, ich werde ihn niemals verwenden, !=weil er eindeutig so viel langsamer ist!" Das ist eine sehr falsche Schlussfolgerung, die er dann weiter verbreiten wird. Deshalb glaube ich, dass Ihre Antwort schädlich ist - sie beantwortet eine falsche Frage und bringt dem ahnungslosen Leser eine falsche Schlussfolgerung. Überlegen Sie, was passiert , wenn Sie Ihre ändern , CheckNullableImplum auch sein return o != null;Sie erhalten die gleiche Benchmark - Ergebnis erhalten.
GSerg
8
Ich streite mit Ihrer Antwort. Ihre Antwort sieht täuschend so aus, als ob sie den Unterschied zwischen !=und HasValuezeigt, obwohl sie tatsächlich den Unterschied zwischen object ound zeigt T? o. Wenn Sie das tun, was ich vorgeschlagen habe, dh umschreiben CheckNullableImplals public static bool CheckNullableImpl<T>(T? o) where T: struct { return o != null; }, erhalten Sie einen Benchmark, der deutlich zeigt, dass er !=viel langsamer ist als !=. Was Sie zu dem Schluss führen sollte, dass es sich bei dem in Ihrer Antwort beschriebenen Problem überhaupt nicht um !=vs handelt HasValue.
GSerg