Що краще: Nullable <T> .HasValue або Nullable <T>! = Null?


437

Я завжди використовував, Nullable<>.HasValueтому що мені сподобалась семантика. Однак останнім часом я працював над чужою кодовою базою, де вони використовувались Nullable<> != nullвиключно замість цього.

Чи є причина використовувати одне над іншим чи це суто перевагу?

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

vs.

  1. int? b;
    if (b != null)
        // ...

9
Я задав таке запитання ... отримав деякі хороші відповіді: stackoverflow.com/questions/633286 / ...
nailitdown

3
Особисто я б користувався, HasValueоскільки думаю, що слова, як правило, читаються, ніж символи. Це все залежить від вас, і що відповідає вашому існуючому стилю.
Джейк Петроулз

1
.HasValueмає більше сенсу, оскільки він позначає тип типу, T?а не тип, який можна звести нанівець, наприклад, рядки.
користувач3791372

Відповіді:


476

Компілятор замінює нульові порівняння на виклик до HasValue, тому немає різниці. Просто робіть те, що легше читається / має більше сенсу для вас та ваших колег.


86
Я додам до цього, "що б більше не відповідало / відповідає існуючому стилю кодування".
Джош Лі

20
Ого. Я ненавиджу цей синтаксичний цукор. int? x = nullдає мені ілюзію, що зведений екземпляр є еталонним типом. Але правда полягає в тому, що Nullable <T> є типом значення. Здається, я отримаю NullReferenceException:: int? x = null; Use(x.HasValue).
KFL

11
@KFL Якщо синтаксичний цукор вас турбує, просто використовуйте Nullable<int>замість цього int?.
Коул Джонсон

24
На ранніх етапах створення програми ви можете подумати, що достатньо використовувати тип нульового значення для зберігання деяких даних, лише щоб через деякий час зрозуміти, що вам потрібний належний клас для ваших цілей. Написавши оригінальний код для порівняння з null, тоді ви маєте перевагу в тому, що вам не потрібно шукати / замінювати кожен виклик HasValue () порівнянням з нулем.
Андерс

15
Дуже нерозумно скаржитися на можливість встановити Nullable на null або порівняти його з null, зважаючи на те, що називається Nullable . Проблема полягає в тому, що люди плутають "еталонний тип" з "може бути недійсним", але це концептуальна плутанина. Майбутній C # матиме нерегульовані типи посилань.
Джим Балтер

48

Я вважаю за краще, (a != null)щоб синтаксис відповідав референтним типам.


11
Який цілком вводить в оману, звичайно, так як Nullable<>це НЕ контрольний тип.
Луаан

9
Так, але факт, як правило, має значення дуже мало, коли ви не перевіряєте.
cbp

31
Це лише введення в оману концептуально заплутаного. Використання послідовного синтаксису для двох різних типів не означає, що вони одного типу. C # має нульові еталонні типи (усі типи посилань на даний момент є нульовими, але це зміниться в майбутньому) та типи значення, що зводиться до нуля. Використання послідовного синтаксису для всіх змінних типів має сенс. Ні в якому разі не випливає, що типи змінних значень є еталонними типами, або що зведені типи посилань - це значення.
Джим Балтер

Я вважаю за краще, HasValueтому що це читабельніше, ніж!= null
Конрад

Консистенція кодування є більш зрозумілою, якщо ви не змішуєте різні стилі написання одного і того ж коду. Оскільки не у всіх місцях є властивість .HasValue, то воно з тих пір використовує! = Null для збільшення сталісті. На мою думку.
ColacX

21

Я провів деякі дослідження з цього приводу різних методів, щоб присвоїти значення нульовому int. Ось що сталося, коли я робив різні речі. Слід уточнити, що відбувається. Майте на увазі: Nullable<something>або скорочення something?- це структура, над якою компілятор, здається, робить багато роботи, щоб ми могли використовувати з null так, як якщо б це був клас.
Як ви побачите нижче, SomeNullable == nullі SomeNullable.HasValueзавжди буде повертати очікувану правду чи хибність. Хоча це не продемонстровано нижче, SomeNullable == 3він також дійсний (якщо припустити, що SomeNullable є int?).
У той час як SomeNullable.Valueотримує нам повідомлення про помилку виконання , якщо ми призначили nullна SomeNullable. Насправді це єдиний випадок, коли зведені нанівець проблеми можуть створити нам проблему, завдяки комбінації перевантажених операторівobject.Equals(obj) метод та оптимізація компілятора та бізнес мавп.

Ось опис деякого коду, яким я керував, і який результат він виробляв у мітках:

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

Гаразд, спробуємо наступний метод ініціалізації:

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

Все те саме, що і раніше. Майте на увазі, що ініціалізація з int? val = new int?(null);нулем, переданим конструктору, спричинила б помилку в часі КОМПЛІЄТНО, оскільки ЗНАЧЕННЯ ЗНАЧЕННЯ об'єкта НЕ зводиться до нуля. Тільки сам об’єкт обгортки може дорівнювати нулю.

Так само ми отримаємо помилку часу компіляції з:

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

не кажучи вже про те, що val.Valueце властивість лише для читання, тобто ми навіть не можемо використовувати щось на зразок:

val.Value = 3;

але знову ж таки, поліморфні перевантажені оператори неявного перетворення дозволяють нам робити:

val = 3;

Не потрібно турбуватися про те, що багато чого може бути, але поки це працює правильно? :)


5
"Майте на увазі: Nullable <щось> або скорочення чогось? - це клас." Це неправильно! Nullable <T> - це структура. Він перевантажує рівний оператор і ==, щоб повернути істину в порівнянні з null. Для цього порівняння компілятор не працює.
andrewjs

1
@andrewjs - Ви праві, що це структура (а не клас), але ви помиляєтесь, що перевантажує оператор ==. Якщо ви введете Nullable<X>VisualStudio 2013 та F12, ви побачите, що це лише перевантажує перетворення до та з X, та Equals(object other)метод. Однак я думаю, що оператор == використовує цей метод за замовчуванням, тому ефект той самий. Я насправді мав намір деякий час оновити цю відповідь на цей факт, але я ледачий та / або зайнятий. Цей коментар доведеться робити зараз :)
Перрін Ларсон

Я зробив швидку перевірку через ildasm, і ви маєте рацію щодо компілятора, який робив якусь магію; порівняння об'єкта Nullable <T> з null насправді означає переклик до HasValue. Цікаво!
andrewjs

3
@andrewjs Насправді компілятор виконує багато робіт, щоб оптимізувати нульові показники. Наприклад, якщо ви присвоїли значення нульовому типу, воно насправді взагалі не буде нульовим (наприклад, int? val = 42; val.GetType() == typeof(int)). Тож не тільки є нульовою структура, яка може бути дорівнює нулю, але також часто взагалі не є нульовою! : D Точно так само, коли ви позначаєте нульове значення, ви боксуєте int, а не int?- а коли int?значення не має, ви отримуєте nullзамість ящикового нульового значення. Це в основному означає, що рідко
виникають

1
@JimBalter Дійсно? Це дуже цікаво. Отже, що говорить профайл пам'яті про нульове поле в класі? Як ви оголошуєте тип значення, який успадковується від іншого типу значення в C #? Як ви заявляєте про власний нульовий тип, який веде себе так само, як і. NULLABLE тип? З тих пір, коли це Nullтип у .NET? Чи можете ви вказати на частину специфікації CLR / C #, де це сказано? Nullables добре визначені в специфікації CLR, їх поведінка - це не "реалізація абстракції" - це контракт . Але якщо найкраще, що ти можеш зробити, це атаки ad hominem, насолоджуйся.
Луань

13

У VB.Net. НЕ використовуйте "IsNot Nothing", коли ви можете використовувати ".HasValue". Я щойно вирішив, що "Операція може дестабілізувати середню помилку виконання", замінивши "IsNot Nothing" на ".HasValue" в одному місці. Я не дуже розумію, чому, але в компіляторі щось відбувається інакше. Я б припустив, що "! = Null" у C # може мати ту саму проблему.


8
Я вважаю за краще HasValueчитати. IsNot Nothingнасправді потворне вираження (через подвійне заперечення).
Стефан Штейнеггер

12
@steffan "IsNot Nothing" не є подвійним запереченням. "Нічого" не є негативним, це дискретна кількість, навіть поза сферою програмування. "Ця кількість - це ніщо". граматично - це саме те саме, що сказати "Ця кількість не дорівнює нулю". і не є подвійним негативним.
jmbpiano

5
Справа не в тому, що я не хочу погоджуватися з відсутністю тут правди, але давай зараз. IsNot Ніщо не є явно, добре, надмірно негативним. Чому б не написати щось позитивне та зрозуміле, як HasValue? Це не граматичний тест, це кодування, де ключовою метою є ясність.
Ренді Гамаж

3
jmbpiano: Я погоджуюсь, що це не подвійне заперечення, але це єдина заперечення, і це майже так некрасиво і не так ясно, як простий позитивний вираз.
Каве Аджарі

0

Якщо ви використовуєте linq і хочете, щоб ваш код був коротким, я рекомендую завжди використовувати !=null

І ось чому:

Уявімо, у нас є клас Fooз нульовою подвійною змінноюSomeDouble

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

Якщо десь у нашому коді ми хочемо отримати все Foo з ненульовими значеннями SomeDouble з колекції Foo (якщо припустимо, що деякі foos у колекції теж можуть бути нульовими), ми закінчимо щонайменше трьома способами написати свою функцію (якщо ми використовувати 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
}

І в такій ситуації я рекомендую завжди йти за коротшою


2
Так, foo?.SomeDouble.HasValueце помилка часу компіляції (не «кидок» у моїй термінології) в цьому контексті, оскільки її тип є bool?, а не просто bool. ( .WhereМетод хоче a Func<Foo, bool>.) Дозволено це робити (foo?.SomeDouble).HasValue, звичайно, оскільки це має тип bool. Це те, що ваш перший рядок "перекладається" на внутрішню компілятором C # (принаймні формально).
Джеппе Стіг Нільсен

-6

Загальна відповідь і правило: якщо у вас є можливість (наприклад, написання користувальницьких серіалізаторів), щоб обробити Nullable в іншому конвеєрі, ніж object- і використовувати їх специфічні властивості - виконайте це і використовуйте властивості Nullable. Тому з точки зору послідовного мислення HasValueслід віддавати перевагу. Послідовне мислення може допомогти вам написати кращий код, не витрачаючи занадто багато часу на деталі. Наприклад, другий метод буде в багато разів більш ефективним (здебільшого через компілятори, що містять вкладиші та бокс, але все ж цифри дуже виразні):

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

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

Тест тестування:

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 |

Код орієнтиру:

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 було використано

PS . Люди кажуть, що поради "віддають перевагу HasValue через послідовне мислення" не пов'язані та марні. Чи можете ви передбачити ефективність цього?

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

PPS Люди продовжують мінус, але ніхто не намагається передбачити ефективність CheckNullableGenericImpl. І там компілятор не допоможе замінити !=nullз HasValue. HasValueслід використовувати безпосередньо, якщо ви зацікавлені у виконанні.


2
Ваші CheckObjectImpl ящики зводяться до нуля object, тоді як CheckNullableImplбокс не використовується. Таким чином, порівняння дуже несправедливе. Мало того, що це не плата за проїзд, а й марно , тому що, як зазначалося в загальноприйнятому відповідь , компілятор переписує !=в HasValueбудь-якому випадку.
GSerg

2
Читачі не ігнорують структурну природу Nullable<T>, яку ви робите (помітивши її в анкету object). Якщо ви застосовуєте != nullз нульовим зліва зліва, бокс не виникає, тому що підтримка !=nullables працює на рівні компілятора. Це інакше, коли ви ховаєте нульовий від компілятора, попередньо встановивши його в поле object. Ні, CheckObjectImpl(object o)ні ваш орієнтир в принципі не мають сенсу.
GSerg

3
Моя проблема полягає в тому, що я дбаю про якість контенту на цьому веб-сайті. Те, що ви опублікували, або вводить в оману, або неправильно. Якщо ви намагалися відповісти на питання ОП, то ваша відповідь є невірною, що легко довести, замінивши виклик на CheckObjectImplйого корпус всередині CheckObject. Однак ваші останні коментарі показують, що ви насправді мали на увазі зовсім інше питання, коли ви вирішили відповісти на це 8-річне запитання, що робить вашу відповідь оманливою в контексті початкового питання. Про це не просили ОП.
GSerg

3
Поставте себе у взутті наступного хлопця, який гуглить what is faster != or HasValue. Він приходить до цього питання, переглядає вашу відповідь, цінує ваш орієнтир і каже: "Джи, я ніколи не буду користуватися, !=тому що це явно набагато повільніше!" Це дуже неправильний висновок, який він потім продовжить поширювати навколо. Ось чому я вважаю, що ваша відповідь шкідлива - вона відповідає на неправильне запитання і, таким чином, наводить неправильний висновок у не підозрілого читача. Розглянемо , що відбувається , коли ви міняєте , CheckNullableImplщоб також бути return o != null;ви отримаєте той же результат тесту.
GSerg

8
Я сперечаюся з вашою відповіддю. Ваш відповідь оманливе виглядає , як це показує різницю між !=і HasValueколи насправді це показує різницю між object oі T? o. Якщо ви зробите те, що я запропонував, тобто переписати CheckNullableImplяк public static bool CheckNullableImpl<T>(T? o) where T: struct { return o != null; }, ви отримаєте тест, який чітко показує, що !=це набагато повільніше, ніж !=. Який повинен привести вас до висновку , що питання ваш відповідь описує не про !=проти HasValueвзагалі.
GSerg
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.