Что предпочтительнее: Nullable <T> .HasValue или Nullable <T>! = Null?

437

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

Есть ли причина использовать один над другим или это просто предпочтение?

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

против

  1. int? b;
    if (b != null)
        // ...
    
ЖХ.
источник
9
Я задал похожий вопрос ... получил несколько хороших ответов: stackoverflow.com/questions/633286/…
nailitdown
3
Лично я бы использовал, HasValueтак как я думаю, что слова, как правило, более читабельны, чем символы. Это все зависит от вас, и то, что соответствует вашему существующему стилю.
Джейк Петрулес
1
.HasValueимеет больше смысла, так как обозначает, что тип имеет тип, T?а не тип, который может быть обнуляемым, например строки.
user3791372

Ответы:

476

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

Рекс М
источник
86
Я хотел бы добавить к этому «какой бы ни был более последовательный / следует существующий стиль кодирования».
Джош Ли
20
Ух ты. Я ненавижу этот синтаксический сахар. int? x = nullдает мне иллюзию, что обнуляемый экземпляр является ссылочным типом. Но правда в том, что Nullable <T> является типом значения. Он чувствует , что я получаю NullReferenceException сделать: int? x = null; Use(x.HasValue).
KFL
11
@KFL Если вам мешает синтаксический сахар, просто используйте Nullable<int>вместо int?.
Коул Джонсон
24
На ранних этапах создания приложения вы можете подумать, что достаточно использовать тип значения Nullable для хранения некоторых данных, но только через некоторое время вы поймете, что вам нужен подходящий класс для вашей цели. Написав исходный код для сравнения с нулем, вы получаете то преимущество, что вам не нужно искать / заменять каждый вызов HasValue () на нулевое сравнение.
Андерс
15
Довольно глупо жаловаться на возможность установить Nullable на ноль или сравнить его с NULL, учитывая, что он называется Nullable . Проблема в том, что люди связывают «ссылочный тип» с «может быть нулевым», но это концептуальная путаница. Будущий C # будет иметь необнуляемые ссылочные типы.
Джим Балтер
48

Я предпочитаю, (a != null)чтобы синтаксис соответствовал ссылочным типам.

CBP
источник
11
Который вполне вводит в заблуждение, конечно, так как Nullable<>это не ссылочный тип.
Луаан
9
Да, но тот факт, как правило, очень мало имеет значения в момент, когда вы проверяете ноль.
Cbp
31
Это только вводит в заблуждение концептуально запутанных. Использование согласованного синтаксиса для двух разных типов не означает, что они одного типа. В C # имеются допустимые для использования ссылочные типы (все ссылочные типы в настоящее время обнуляются, но это изменится в будущем) и типы значений, допускающие значение NULL. Использование согласованного синтаксиса для всех обнуляемых типов имеет смысл. Это никоим образом не означает, что типы значений, допускающие значение NULL, являются ссылочными типами или что типы ссылок, допускающие значения NULL, являются типами значений.
Джим Балтер
Я предпочитаю, HasValueпотому что это более читабельно, чем!= null
Конрад
Согласованность кода более удобочитаема, если вы не смешиваете разные стили написания одного и того же кода. Так как не во всех местах есть свойство .HasValue, оно делает использование! = Null для повышения согласованности. По моему мнению.
ColacX
21

Я провел некоторое исследование по этому вопросу, используя различные методы для присвоения значений обнуляемому int. Вот что случилось, когда я делал разные вещи. Следует уточнить, что происходит. Имейте в виду: Nullable<something>или сокращение something?- это структура, для которой компилятор, кажется, проделывает большую работу, чтобы позволить нам использовать с нулем, как если бы это был класс.
Как вы увидите ниже, 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);нулем, переданным в конструктор, привела бы к ошибке времени COMPILE, так как значение VALUE объекта, допускающего значение NULL, НЕ может иметь значение NULL. Только сам объект-оболочка может равняться нулю.

Аналогично, мы получили бы ошибку времени компиляции из:

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

не говоря уже о том, что val.Valueэто свойство только для чтения, то есть мы не можем даже использовать что-то вроде:

val.Value = 3;

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

val = 3;

Не нужно беспокоиться о том, что может случиться, если это работает правильно? :)

Перрин Ларсон
источник
5
«Имейте в виду: Nullable <что-то> или сокращение что-то? Это класс». Это не верно! Nullable <T> является структурой. Он перегружает операторы Equals и == для возврата true по сравнению с нулем. Для этого сравнения компилятор не работает.
andrewjs
1
@andrewjs - Вы правы, что это структура (а не класс), но вы не правы, что она перегружает оператор ==. Если вы Nullable<X>наберете VisualStudio 2013 и F12, вы увидите, что он перегружает только преобразование Xв Equals(object other)метод и из него и метод. Тем не менее, я думаю, что оператор == использует этот метод по умолчанию, поэтому эффект тот же. Я действительно хотел обновить этот ответ некоторое время, но я ленив и / или занят. Этот комментарий придется сделать сейчас :)
Перрин Ларсон
Я быстро проверил ildasm, и вы правы в том, что компилятор совершил какое-то волшебство; сравнение объекта Nullable <T> с нулем фактически переводит вызов в HasValue. Интересно!
andrewjs
3
@andrewjs На самом деле, компилятор выполняет огромную работу по оптимизации значений Nullables. Например, если вы присваиваете значение обнуляемому типу, оно фактически не будет обнуляемым вообще (например, int? val = 42; val.GetType() == typeof(int)). Так что не только nullable структура, которая может быть равна нулю, это также часто вообще не nullable! : D Точно так же, когда вы упаковываете значение, допускающее значение NULL, вы упаковываете int, а не int?- и когда int?оно не имеет значения, вы получаете nullвместо значения в штучной упаковке значение NULL. По сути, это означает, что при правильном использовании
nullable
1
@JimBalter Правда? Это очень интересно. Так что же профилировщик памяти скажет вам об обнуляемом поле в классе? Как вы объявляете тип значения, который наследуется от другого типа значения в C #? Как вы объявляете свой собственный обнуляемый тип, который ведет себя так же, как обнуляемый тип .NET? С каких это пор Nullтип в .NET? Можете ли вы указать на часть в спецификации CLR / C #, где это сказано? Nullables хорошо определены в спецификации CLR, их поведение не является «реализацией абстракции» - это контракт . Но если лучшее, что вы можете сделать - это нападения ad hominem, наслаждайтесь.
Луаан
13

В VB.Net. НЕ используйте «IsNot Nothing», когда вы можете использовать «.HasValue». Я только что решил «Операция могла дестабилизировать среду выполнения» Ошибка среднего доверия, заменив «IsNot Nothing» на «.HasValue» в одном месте. Я не очень понимаю, почему, но что-то происходит по-другому в компиляторе. Я хотел бы предположить, что "! = Нуль" в C # может иметь ту же проблему.

Картер Медлин
источник
8
Я бы предпочел HasValueиз-за читабельности. IsNot Nothingдействительно уродливое выражение (из-за двойного отрицания).
Стефан Штайнеггер
12
@steffan "IsNot Nothing" не является двойным отрицанием. «Ничто» не является отрицательным, это дискретная величина, даже вне сферы программирования. «Это количество не ничто». Грамматически это то же самое, что сказать: «Это количество не равно нулю». и ни один не является двойным негативом.
jmbpiano
5
Дело не в том, что я не хочу не соглашаться с отсутствием правды здесь, а давай сейчас. Ничего не ясно, ну, слишком негативно. Почему бы не написать что-то позитивное и ясное, например, 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со значением nullable слева, бокс не возникает, потому что поддержка !=для nullables работает на уровне компилятора. Другое дело, когда вы скрываете nullable от компилятора, сначала поместив его в object. Ни CheckObjectImpl(object o)ваш эталонный тест не имеет смысла в принципе.
GSerg
3
Моя проблема в том, что я беспокоюсь о качестве контента на этом сайте. То, что вы опубликовали, вводит в заблуждение или неправильно. Если вы пытались ответить на вопрос ОП, то ваш ответ совершенно неправильный, что легко проверить, заменив вызов на CheckObjectImplего тело внутри CheckObject. Однако ваши последние комментарии показывают, что на самом деле вы имели в виду совершенно другой вопрос, когда решили ответить на этот восьмилетний вопрос, что вводит ваш ответ в заблуждение в контексте исходного вопроса. Это было не то, о чем спрашивал ОП.
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