Nullable<T>.HasValue和Nullable<T> != null有什么区别?

528

我一直使用Nullable<>.HasValue,因为我喜欢这个语义。然而,最近我在别人的现有代码库上工作时,他们专门使用Nullable<> != null

使用其中一个的原因是什么,还是纯粹个人偏好?

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

对比。

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

10
我问了一个类似的问题...得到了一些好的答案:https://dev59.com/BHRB5IYBdhLWcg3wZmlz - nailitdown
3
个人而言,我会使用“HasValue”,因为我认为单词比符号更易读。不过这完全取决于你的喜好和现有风格是否匹配。 - Jake Petroules
1
".HasValue" 更有意义,因为它表示类型是 "T?" 类型,而不是可为空的类型,例如字符串。 - user3791372
6个回答

581

编译器会将null比较替换为调用HasValue,因此实际上没有区别。只需选择更易读/更合理的方式即可,以适应您和同事们的习惯。


92
我会将其翻译为:“我会补充一点,即‘选用与现有代码风格相一致的或者更为一致的。’” - Josh Lee
22
哇,我讨厌这种语法糖。int? x = null 会让我产生可空实例是引用类型的错觉。但事实上,Nullable<T> 是值类型。如果这样做:int? x = null; Use(x.HasValue),我会感觉会收到一个 NullReferenceException 异常。 - KFL
15
如果你觉得语法糖让你感到不适,可以使用 Nullable<int> 代替 int? - Cole Tobin
36
在创建应用程序的早期阶段,你可能会认为使用可空值类型来存储某些数据是足够的,但是过一段时间后你会意识到你需要一个适当的类来实现你的目的。编写原始代码以与null进行比较具有优势,这样你就不需要在每次调用HasValue()时搜索/替换为null比较。 - Anders
26
抱怨能够将Nullable设为null或将其与null进行比较是相当愚蠢的,因为它被称为Nullable。问题在于人们混淆了“引用类型”和“可为空”,但这是一个概念上的混淆。未来的C#将具有不可为空的引用类型。 - Jim Balter
显示剩余3条评论

60

我更喜欢使用(a != null),这样语法就与引用类型匹配。


16
这句话含糊其辞,因为 Nullable<> 并不是引用类型。 - Luaan
12
是的,但通常事实在你进行空值检查时很少起到重要作用。 - cbp
50
只有在概念上混淆的情况下才会产生误导。对于两种不同类型使用一致的语法并不意味着它们是相同的类型。C#拥有可空引用类型(目前所有引用类型都是可空的,但将来会改变)和可空值类型。对于所有可空类型使用一致的语法是有意义的。这绝不意味着可空值类型是引用类型,或者可空引用类型是值类型。 - Jim Balter
1
编码一致性更易读,如果您不混合不同的编写风格来编写相同的代码。由于并非所有地方都有 .HasValue 属性,因此使用 != null 可以增加一致性。这是我的观点。 - ColacX
2
如果没有其他原因,一定要投票支持这个偏好设置,因为它可以使代码更简单。当从引用类型转换为可空类型时,不需要在任何其他地方进行代码更改,而使用.HasValue则会成为不正确的语法,一旦它不再明确为“Nullable”,这可能不是常见情况,但如果您曾经为了Tuple而编写了一个结构体,然后将其转换为类,则已经处于适用此设置的区域,并且随着NullableRefs的出现,这种情况将变得更加普遍。 - Captain Prinny
显示剩余4条评论

23

我通过使用不同的方法为可空int类型分配值进行了一些研究。以下是当我执行各种操作时发生的情况。应该澄清正在发生的事情。

请记住:Nullable<something>或简写something?是一个结构体,编译器似乎在做很多工作,让我们像使用类一样使用null。

如下所示,SomeNullable == nullSomeNullable.HasValue始终会返回预期的true或false。虽然下面没有演示,但SomeNullable == 3也是有效的(假设SomeNullable是一个int?)。

当我们将null分配给SomeNullable时,SomeNullable.Value会给我们带来一个运行时错误。这实际上是唯一可能会导致问题的情况,由于重载的运算符,重载的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); 进行初始化,并将 null 传递给构造函数,将会导致编译时错误,因为可空对象的值是不可空的。只有包装对象本身才可以等于 null。

同样地,以下代码也会导致编译时错误:

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

更不用说 val.Value 是只读属性,因此我们甚至不能使用以下方式:
val.Value = 3;

但同样,多态重载的隐式转换运算符让我们能够做到:

val = 3;

不需要担心 polysomthing whatchamacallits,只要它能正常工作就好了? :)

9
请记住:Nullable<something> 或其简写 something? 是一个结构体,而不是类。它重载了 Equals 和 == 运算符,使得与 null 比较时返回 true。编译器并不会为此比较做任何特殊处理。 - andrewjsaid
1
@andrewjs - 你说得没错,它是一个结构体(不是类),但你错了,它没有重载==运算符。如果你在VisualStudio 2013中输入Nullable<X>并按F12键,你会发现它只重载了转换为和从X的方法以及Equals(object other)方法。然而,我认为==运算符默认使用该方法,所以效果是相同的。实际上,我一直想更新这个答案的事实,但我懒得或者太忙了。这个评论暂时就这样吧 :) - Perrin Larson
我通过ildasm进行了快速检查,你关于编译器做了一些魔法是正确的;将Nullable<T>对象与null进行比较实际上会转换为调用HasValue。有趣! - andrewjsaid
3
实际上,编译器会花费大量功夫来优化可空类型。例如,如果将值分配给可空类型,则实际上它根本不是可空的(例如,int? val = 42; val.GetType() == typeof(int))。因此,可空结构既可以等于null,也经常根本不是可为空!:D 同样地,当对可空值进行装箱时,你将装箱为int而不是int? - 当int?没有值时,你会得到null而不是装箱的可空值。这基本上意味着使用可空类型几乎没有开销 :) - Luaan
1
@JimBalter 真的吗?那很有趣。那么内存分析器对类中的可空字段有什么告诉你的呢?在C#中如何声明继承自另一个值类型的值类型?如何声明自己的可空类型,使其与.NET的可空类型行为相同?从何时起Null是.NET中的一种类型?你能指出CLR/C#规范中说到这一点的部分吗?可空类型在CLR规范中有明确定义,它们的行为不是“抽象的实现”,而是契约。但如果你最好的办法只是进行人身攻击,那就尽情享受吧。 - Luaan
显示剩余4条评论

15
在VB.Net中,如果可以使用.HasValue,请勿使用IsNot Nothing。我刚刚通过在一个位置上用.HasValue替换IsNot Nothing解决了一个“操作可能破坏运行时”的中等信任错误。我不是很理解为什么会这样,但编译器中发生了一些不同的事情。我会假设在C#中!= null可能会有相同的问题。

12
出于易读性的考虑,我更倾向于使用 HasValueIsNot Nothing 是一个很丑陋的表达式(因为有双重否定)。 - Stefan Steinegger
12
@steffan "IsNot Nothing" 不是双重否定。"Nothing" 不是负数,它是一个离散的数量,甚至在编程领域之外也是如此。“这个数量不是空。”从语法上讲,与说“这个数量不是零。”完全相同,两者都不是双重否定。 - jmbpiano
8
我并不是不想反对这里缺乏真相的说法,但现在得明白了。IsNot Nothing 显然过于消极了,为什么不写一些积极而清晰的东西,比如 HasValue 呢?这不是语法测试,而是编码,关键目标是清晰易懂。 - Randy Gamage
5
我同意这不是双重否定,但它是单一否定,比起简单的肯定表达来说,这几乎一样难以理解且没有那么清晰易懂。 - Kaveh Hadjari

0
如果您使用linq并希望保持代码简短,我建议始终使用!= null 原因如下:
假设我们有一个名为Foo的类,其中包含一个可空的双精度变量SomeDouble
public class Foo
{
    public double? SomeDouble;
    //some other properties
}   

如果在我们的代码中,我们想要从一组Foo中获取所有具有非空SomeDouble值的Foo(假设集合中的某些Foo也可以为null),那么我们最少有三种方法可以编写函数(如果使用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
}

在这种情况下,我建议始终选择较短的那个。

3
是的,在那个上下文中,foo?.SomeDouble.HasValue 是一个编译时错误(在我的术语中不是“抛出异常”),因为它的类型是 bool?,而不仅仅是 bool。(.Where 方法需要一个 Func<Foo, bool>。)当然,做 (foo?.SomeDouble).HasValue 是允许的,因为它的类型是 bool。这就是你的第一行代码被 C# 编译器内部“翻译”成的形式(至少是正式的)。 - Jeppe Stig Nielsen

-8

第二种方法将会更加有效(主要因为编译器的内联和装箱,但数字仍然非常具有表现力):

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

因此,如果您有一种选择(例如编写自定义序列化程序),可以在与object不同的管道中处理Nullable并使用其特定属性,请这样做并使用Nullable的特定属性。从一致的思考角度来看,应优先考虑HasValue。一致的思考有助于您编写更好的代码,不需要在细节上花费太多时间。

PS。人们说,“出于一致的思考考虑而优先选择HasValue”这个建议与主题无关且毫无用处。 您能预测这会对性能产生什么影响吗?

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

PPS 人们继续减少,似乎没有人尝试预测 CheckNullableGenericImpl 的性能。我告诉你:编译器不会帮助你用 HasValue 替换 !=null。如果你关心性能,应该直接使用 HasValue


4
您的 CheckObjectImpl boxes 可空值为 object,而 CheckNullableImpl 则不使用装箱。因此比较是非常不公平的。它不仅不公平,而且也是没有用的,因为正如被接受的答案中所指出的那样,编译器会将 != 重写为 HasValue - GSerg
3
读者不应忽略 Nullable<T> 的结构特性,但是您通过将其装箱为 object 来忽略了它。当您在可空类型的左侧应用 != null 时,由于可空类型对 != 的支持在编译器级别工作,所以不会发生装箱操作。但是,当您先将可空类型装箱为 object 后再隐藏它时,情况就不同了。从原则上讲,CheckObjectImpl(object o) 和您的基准测试都没有意义。 - GSerg
4
我的问题是,我关注这个网站上的内容质量。你发布的内容要么具有误导性,要么是错误的。如果你试图回答提问者的问题,那么你的答案完全是错的,这可以通过在CheckObject中替换调用CheckObjectImpl的方法体来轻松证明。然而,你最近的评论揭示了一个完全不同的问题,当你决定回答这个8年前的问题时,这使得你的答案在原始问题的背景下具有误导性。这不是提问者所问的。 - GSerg
4
请你假设自己是下一个使用 Google 搜索“什么更快,!= 还是 HasValue” 的人。当他看到这个问题并浏览了你的回答后,他会感谢你提供的基准测试,并且会得出一个错误的结论,认为“哇,我以后再也不会使用 '!=' 了,因为它明显更慢!”。这是一个非常错误的结论,他可能会继续传播这个错误的结论。这就是为什么我认为你的回答是有害的——它回答了一个错误的问题,因此给毫无戒备的读者留下了错误的结论。想象一下当你将 CheckNullableImpl 改成 return o != null; 后会发生什么。你将得到相同的基准测试结果。 - GSerg
11
我对你的回答提出异议。你的回答外表欺骗性地显示了!=HasValue之间的区别,但实际上它显示了object oT? o之间的区别。如果按照我所建议的,重新编写CheckNullableImpl成为public static bool CheckNullableImpl<T>(T? o) where T: struct { return o != null; },你最终会得到一个清晰地显示!=HasValue更慢的基准测试结果。这应该让你得出结论,你的回答描述的问题根本不是关于!=HasValue之间的问题。 - GSerg
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接