Który jest preferowany: Nullable <T> .HasValue lub Nullable <T>! = Null?

437

Zawsze używałem, Nullable<>.HasValueponieważ lubiłem semantykę. Jednak ostatnio pracowałem nad czyjąś bazą kodów, w której używały one Nullable<> != nullwyłącznie zamiast tego.

Czy istnieje powód, aby używać jednego nad drugim, czy to czysta preferencja?

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

vs.

  1. int? b;
    if (b != null)
        // ...
    
lc.
źródło
9
Zadałem podobne pytanie ... otrzymałem kilka dobrych odpowiedzi: stackoverflow.com/questions/633286/...
nailitdown
3
Osobiście skorzystałbym, HasValueponieważ uważam, że słowa są bardziej czytelne niż symbole. Wszystko zależy od Ciebie i od tego, co pasuje do Twojego stylu.
Jake Petroules,
1
.HasValuema większy sens, ponieważ oznacza, że ​​typ jest typu, T?a nie typu, który może być zerowalny, np. ciągi znaków.
user3791372,

Odpowiedzi:

476

Kompilator zastępuje porównania zerowe wywołaniem do HasValue, więc nie ma prawdziwej różnicy. Po prostu rób to, co jest bardziej czytelne / ma sens dla ciebie i twoich kolegów.

Rex M.
źródło
86
Dodałbym do tego, że „którykolwiek jest bardziej spójny / zgodny z istniejącym stylem kodowania”.
Josh Lee
20
Łał. Nienawidzę tego cukru syntaktycznego. int? x = nulldaje mi złudzenie, że instancja zerowalna jest typem referencyjnym. Ale prawda jest taka, że ​​Nullable <T> jest typem wartości. Czuje że mam NullReferenceException zrobić: int? x = null; Use(x.HasValue).
KFL
11
@KFL Jeśli przeszkadza Ci cukier syntaktyczny, po prostu użyj Nullable<int>zamiast int?.
Cole Johnson
24
Na wczesnych etapach tworzenia aplikacji możesz pomyśleć, że wystarczy użyć wartości dopuszczającej wartości zerowe do przechowywania niektórych danych, ale dopiero po pewnym czasie uświadomisz sobie, że potrzebujesz odpowiedniej klasy do swoich celów. Po napisaniu oryginalnego kodu do porównania z null ma tę zaletę, że nie trzeba wyszukiwać / zamieniać każdego wywołania HasValue () z porównaniem null.
Anders
15
To dość głupie narzekać jest w stanie ustawić pustych null lub porównać go do wartości null danego to nazywa Nullable . Problem polega na tym, że ludzie łączą „typ odniesienia” z „może być zerowy”, ale jest to zamieszanie koncepcyjne. Przyszłe C # będzie mieć typy referencyjne, które nie mają wartości zerowej.
Jim Balter,
48

Wolę, (a != null)aby składnia pasowała do typów referencji.

cbp
źródło
11
Co oczywiście jest dość mylące, ponieważ nieNullable<> jest to typ odniesienia.
Luaan,
9
Tak, ale fakt zwykle nie ma większego znaczenia w momencie, gdy sprawdzasz wartość zerową.
cbp
31
Jest to mylące tylko dla konceptualnie zdezorientowanych. Użycie spójnej składni dla dwóch różnych typów nie oznacza, że ​​są one tego samego typu. C # ma typy zerowalne (wszystkie typy referencyjne są obecnie zerowalne, ale to się zmieni w przyszłości) i typy zerowalne. Zastosowanie spójnej składni dla wszystkich typów dopuszczających wartości zerowe ma sens. W żaden sposób nie oznacza to, że typy wartości dopuszczających wartości zerowe są typami referencyjnymi lub że typy wartości dopuszczających wartości zerowe są typami wartości.
Jim Balter,
Wolę, HasValueponieważ jest bardziej czytelny niż!= null
Konrad
Konsystencja kodowania jest bardziej czytelna, jeśli nie łączysz różnych stylów pisania tego samego kodu. Ponieważ nie wszystkie miejsca mają właściwość .HasValue, dlatego odtąd należy używać! = Null dla zwiększenia spójności. W mojej opinii.
ColacX
21

Zrobiłem trochę badań na ten temat, używając różnych metod, aby przypisać wartości do wartości zerowej int. Oto, co się stało, gdy zrobiłem różne rzeczy. Powinien wyjaśnić, co się dzieje. Pamiętaj: Nullable<something>albo stenografia something?jest strukturą, dla której kompilator wydaje się wykonywać wiele pracy, abyśmy mogli używać wartości zerowej, jakby to była klasa.
Jak zobaczycie poniżej, SomeNullable == nulli SomeNullable.HasValuezawsze zwraca oczekiwane prawdziwe lub fałszywe. Chociaż nie pokazano poniżej, SomeNullable == 3jest również poprawny (zakładając, że SomeNullable jest an int?).
Podczas gdy SomeNullable.Valuedostaje nam błąd czasu wykonania, jeśli przypisaliśmy nulldo SomeNullable. Jest to w rzeczywistości jedyny przypadek, w którym wartości zerowe mogą sprawić nam problem, dzięki kombinacji przeciążonych operatorów, przeciążonychobject.Equals(obj) metoda oraz optymalizacja kompilatora i małpia firma.

Oto opis jakiegoś kodu, który uruchomiłem, i jaki wynik wygenerował w etykietach:

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, spróbujmy następnej metody inicjalizacji:

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.")

Wszystko tak samo jak poprzednio. Należy pamiętać, że inicjowanie za pomocą int? val = new int?(null);, z pustym przekazanym do konstruktora, spowodowałoby błąd czasowy KOMPILACJA, ponieważ WARTOŚĆ obiektu zerowalnego NIE jest dopuszczalna. Tylko sam obiekt otoki może mieć wartość null.

Podobnie otrzymalibyśmy błąd czasu kompilacji z:

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

nie wspominając, że i tak val.Valuejest to właściwość tylko do odczytu, co oznacza, że ​​nie możemy nawet użyć czegoś takiego:

val.Value = 3;

ale znowu polimorficzne przeciążone niejawne operatory konwersji pozwalają nam na:

val = 3;

Nie musisz się jednak martwić o wieloskładnikowe, o ile działa dobrze? :)

Perrin Larson
źródło
5
„Pamiętaj: Nullable <coś> lub stenografia coś? Jest klasą.” To jest źle! Nullable <T> jest strukturą. Przeciąża operator Equals i ==, aby zwracać wartość true w porównaniu do wartości null. Kompilator nie wykonuje żadnej wymyślnej pracy dla tego porównania.
andrewjs
1
@andrewjs - Masz rację, że jest to struct (nie klasa), ale mylisz się, że przeciąża on operator ==. Jeśli wpiszesz Nullable<X>w VisualStudio 2013 i F12, zobaczysz, że przeciąża tylko konwersję do i z Xoraz Equals(object other)metodę. Myślę jednak, że operator == domyślnie używa tej metody, więc efekt jest taki sam. Już od jakiegoś czasu chciałem zaktualizować tę odpowiedź, ale jestem leniwy i / lub zajęty. Ten komentarz na razie będzie musiał zrobić :)
Perrin Larson,
Zrobiłem szybkie sprawdzenie przez ildasm i masz rację, że kompilator robi magię; porównanie Nullable <T> obiektu z null w rzeczywistości przekłada się na wywołanie HasValue. Ciekawy!
andrewjs
3
@andrewjs W rzeczywistości kompilator wykonuje mnóstwo pracy, aby zoptymalizować wartości zerowe. Na przykład, jeśli przypiszesz wartość do typu zerowalnego, tak naprawdę wcale nie będzie to zerowalne (np int? val = 42; val.GetType() == typeof(int).). Więc nie tylko jest zerowalne strukturą, która może być równa zeru, ale często nie jest wcale zerowalne! : D W ten sam sposób, gdy umieszczasz w polu wartość zerowalną, boksujesz int, a nie int?- a gdy int?nie ma ona wartości, otrzymujesz nullzamiast wartości zerowanej w pudełku. Zasadniczo oznacza to, że właściwie nie ma narzutów związanych z prawidłowym użyciem
zerowania
1
@JimBalter Naprawdę? To bardzo interesujące. Co więc profilujący pamięć mówi o polu zerowalnym w klasie? Jak zadeklarować typ wartości, który dziedziczy po innym typie wartości w języku C #? Jak zadeklarować własny typ zerowalny, który zachowuje się tak samo jak typ zerowalny .NET? Od kiedy jest Nulltyp .NET? Czy możesz wskazać część specyfikacji CLR / C #, w której jest to powiedziane? Wartości null są dobrze zdefiniowane w specyfikacji CLR, ich zachowanie nie jest „implementacją abstrakcji” - to umowa . Ale jeśli najlepsze, co możesz zrobić, to ataki ad hominem, baw się dobrze.
Luaan,
13

W VB.Net. NIE używaj „IsNot Nothing”, gdy możesz użyć „.HasValue”. Właśnie rozwiązałem problem „Operacja może zdestabilizować środowisko uruchomieniowe” Średni błąd zaufania, zastępując „IsNot Nothing” słowem „.HasValue” w jednym miejscu. Naprawdę nie rozumiem dlaczego, ale w kompilatorze dzieje się inaczej. Zakładam, że „! = Null” w języku C # może mieć ten sam problem.

Carter Medlin
źródło
8
Wolałbym HasValueze względu na czytelność. IsNot Nothingjest naprawdę brzydkim wyrażeniem (z powodu podwójnej negacji).
Stefan Steinegger,
12
@steffan „IsNot Nothing” nie jest podwójną negacją. „Nic” nie jest ujemne, jest dyskretną wielkością, nawet poza sferą programowania. „Ta ilość nie jest niczym”. to gramatycznie dokładnie to samo, co powiedzenie „Ta liczba nie jest zerem”. i żaden z nich nie jest podwójnie ujemny.
jmbpiano,
5
Nie chodzi o to, że nie chcę się nie zgadzać z brakiem prawdy tutaj, ale daj spokój. IsNot Nic nie jest wyraźnie, nadmiernie negatywne. Dlaczego nie napisać czegoś pozytywnego i jasnego, takiego jak HasValue? To nie jest test gramatyczny, lecz kodowanie, w którym kluczowym celem jest klarowność.
Randy Gamage,
3
jmbpiano: Zgadzam się, że to nie jest podwójna negacja, ale pojedyncza negacja, która jest prawie tak brzydka i nie tak wyraźna jak zwykłe pozytywne wyrażenie.
Kaveh Hadjari
0

Jeśli używasz linq i chcesz skrócić swój kod, polecam zawsze używać !=null

I dlatego:

Wyobraźmy sobie, że mamy klasę Fooz podwójną zmienną nullSomeDouble

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

Jeśli gdzieś w naszym kodzie chcemy uzyskać wszystkie Foo z wartością zerową SomeDouble z kolekcji Foo (zakładając, że niektóre foos w kolekcji również mogą mieć wartość null), otrzymamy co najmniej trzy sposoby na napisanie naszej funkcji (jeśli użyj 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
}

I w takiej sytuacji polecam zawsze wybierać krótszą

Yan Yankelevich
źródło
2
Tak, foo?.SomeDouble.HasValuew tym kontekście występuje błąd czasu kompilacji (nie „rzut” w mojej terminologii), ponieważ jego typ to bool?nie tylko bool. ( .WhereMetoda chce a Func<Foo, bool>.) Oczywiście można to zrobić (foo?.SomeDouble).HasValue, ponieważ ma on typ bool. Oto, jak twój pierwszy wiersz jest „tłumaczony” wewnętrznie przez kompilator C # (przynajmniej formalnie).
Jeppe Stig Nielsen
-6

Ogólna odpowiedź i ogólna zasada: jeśli masz opcję (np. Pisanie niestandardowych serializatorów), aby przetworzyć wartość Nullable w innym potoku niż object- i użyć ich określonych właściwości - zrób to i użyj właściwości Nullable. Dlatego z punktu widzenia konsekwentnego myślenia HasValuenależy preferować. Konsekwentne myślenie może pomóc Ci napisać lepszy kod, nie poświęcając zbyt wiele czasu na szczegóły. Np. Druga metoda będzie wielokrotnie skuteczniejsza (głównie ze względu na wstawianie i boksowanie kompilatorów, ale wciąż liczby są bardzo wyraziste):

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

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

Test porównawczy:

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 |

Kod testu porównawczego:

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

Wykorzystano https://github.com/dotnet/BenchmarkDotNet

PS . Ludzie mówią, że rada „wolę HasValue ze względu na konsekwentne myślenie” nie jest powiązana i bezużyteczna. Czy potrafisz przewidzieć wydajność tego?

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

Ludzie PPS kontynuują minus, ale nikt nie próbuje przewidzieć wydajności CheckNullableGenericImpl. I tam kompilator nie pomoże wymianie !=nullz HasValue. HasValuenależy stosować bezpośrednio, jeśli jesteś zainteresowany wydajnością.

Roman Pokrovskij
źródło
2
Twoje CheckObjectImpl pola nullable na object, podczas CheckNullableImplgdy nie używa boks. Zatem porównanie jest bardzo niejednoznaczne. Nie tylko nie jest taryfą, jest także bezużyteczne, ponieważ, jak zauważono w przyjętej odpowiedzi , kompilator !=i HasValuetak przepisuje .
GSerg
2
Czytelnicy nie ignorują struktury tego Nullable<T>, co robisz (umieszczając go w polu object). Po zastosowaniu != nullze znakiem zerowym po lewej stronie nie występuje boksowanie, ponieważ obsługa wartości !=zerowych działa na poziomie kompilatora. Jest inaczej, gdy ukrywasz zerowalną wartość przed kompilatorem, najpierw umieszczając ją w polu object. Ani CheckObjectImpl(object o)twój test porównawczy nie ma w zasadzie sensu.
GSerg
3
Mój problem polega na tym, że zależy mi na jakości treści na tej stronie. To, co opublikowałeś, jest mylące lub błędne. Jeśli próbujesz odpowiedzieć na pytanie OP, twoja odpowiedź jest całkowicie błędna, co łatwo udowodnić, zastępując wezwanie do CheckObjectImpljego ciałem w środku CheckObject. Jednak twoje ostatnie komentarze ujawniają, że w rzeczywistości miałeś na myśli zupełnie inne pytanie, kiedy zdecydowałeś się odpowiedzieć na to 8-letnie pytanie, co sprawia, że ​​twoja odpowiedź jest myląca w kontekście pierwotnego pytania. Nie o to pytał PO.
GSerg
3
Postaw się w sytuacji następnego faceta, który google what is faster != or HasValue. Dochodzi do tego pytania, przegląda twoją odpowiedź, docenia twój punkt odniesienia i mówi: „Rany, nigdy nie użyję, !=ponieważ jest o wiele wolniejszy!” To bardzo błędny wniosek, który następnie będzie się rozprzestrzeniał. Dlatego uważam, że twoja odpowiedź jest szkodliwa - odpowiada na złe pytanie, a tym samym wysuwa błędny wniosek u niczego niepodejrzewającego czytelnika. Zastanów się, co się dzieje, kiedy zmienić CheckNullableImpl, aby również być return o != null;Dostaniesz ten sam wynik benchmarku.
GSerg
8
Kłócę się z twoją odpowiedzią. Twoja odpowiedź zwodniczo wygląda, jakby pokazuje różnicę między, !=a HasValuekiedy w rzeczywistości pokazuje różnicę między object oi T? o. Jeśli zrobisz to, co zasugerowałem, to znaczy przepisz CheckNullableImpljako public static bool CheckNullableImpl<T>(T? o) where T: struct { return o != null; }, skończysz testem porównawczym, który wyraźnie pokazuje, że !=jest znacznie wolniejszy niż !=. Co powinno doprowadzić cię do wniosku, że problem, który opisuje twoja odpowiedź, w ogóle nie dotyczy !=kontra HasValue.
GSerg