Which is preferred: Nullable<T>.HasValue or Nullable<T> != null?
我一直使用Nullable<>.HasValue,因为我喜欢语义。然而,最近我正在研究其他人现有的代码库,他们只使用Nullable<> != null。
是否有理由使用其中一个而不是另一个,或者它纯粹是偏好?
1 2 3
| int? a;
if (a.HasValue)
// ... |
VS
1 2 3
| int? b;
if (b != null)
// ... |
- 我问了一个类似的问题…得到了一些很好的答案:stackoverflow.com/questions/633286/&hellip;
- 就我个人而言,我会使用HasValue,因为我认为单词比符号更容易阅读。但这一切都取决于你自己,以及什么适合你现有的风格。
- .HasValue更有意义,因为它表示类型是T?类型,而不是可以为空的类型,如字符串。
编译器将空比较替换为对EDOCX1的调用(0),因此没有真正的区别。只要做你和你的同事更易读/更有意义的事情。
- 我还要补充一点,"无论哪个更一致/遵循现有的编码风格。"
- 真的。我讨厌这种句法上的甜言蜜语。int? x = null给了我一种错觉,即可以为空的实例是引用类型。但事实是,nullable是一种值类型。我觉得我会得到一个空引用例外:int? x = null; Use(x.HasValue)。
- @KFL如果句法上的糖分让你不舒服,就用Nullable代替int?。
- 在创建应用程序的早期阶段,您可能认为使用一个可以为空的值类型来存储一些数据是足够的,只是在一段时间后才意识到您需要一个适合自己用途的类。编写了用于与空进行比较的原始代码之后,您就不需要用空比较来搜索/替换对hasValue()的每个调用了。
- 抱怨能够将null able设置为空或者将其与空进行比较是非常愚蠢的,因为它被称为null able。问题是人们将"引用类型"与"can be null"混为一谈,但这是概念上的混淆。未来的C将具有不可为空的引用类型。
- @kfl i有时使用var z = x ?? y来表示可以为空的类型。在这种情况下,您还喜欢使用var z = x.HasValue ? x : y吗?看起来有点奇怪。
- @从KFL的例子来看,正是= null部分有点奇怪。如果null是Nullable的一个实例,这样你就可以预料到null.HasValue() == false的作用,那么x.HasValue()的作用就不那么奇怪了。
我更喜欢(a != null)以便语法匹配引用类型。
- 当然,这是非常误导的,因为Nullable<>不是引用类型。
- 是的,但事实通常在您进行空检查时无关紧要。
- 这只会误导概念混乱的人。对两个不同的类型使用一致的语法并不意味着它们是相同的类型。C具有可以为空的引用类型(所有引用类型当前都可以为空,但将来会改变)和可以为空的值类型。对所有可为空的类型使用一致的语法是有意义的。它绝不意味着可以为空的值类型是引用类型,也不意味着可以为空的引用类型是值类型。
- 我更喜欢HasValue,因为它比!= null更可读。
我用不同的方法为一个可以为空的int赋值,对此做了一些研究。下面是我做各种事情时发生的事情。应该弄清楚发生了什么。请记住:Nullable或简写something?是一个结构,编译器似乎正在为它做大量工作,以便让我们像使用类一样使用空值。
正如您将在下面看到的,SomeNullable == null和SomeNullable.HasValue将始终返回预期的正确或错误。虽然下面没有演示,但SomeNullable == 3也是有效的(假设someNullable是int?。
而如果我们将null分配给SomeNullable,SomeNullable.Value会给我们一个运行时错误。事实上,这是唯一一个可以为空值的情况,因为重载的操作符、重载的object.Equals(obj)方法、编译器优化和monkey业务的组合会导致我们出现问题。
下面是我运行的一些代码的描述,以及它在标签中生成的输出:
1 2 3 4 5 6 7 8 9
| 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.") |
好,让我们尝试下一个初始化方法:
1 2 3 4 5 6 7 8 9
| 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传递给构造函数,则会产生编译时错误,因为可以为空的对象的值不可以为空。只有包装对象本身可以等于空。
同样,我们将从以下位置得到编译时错误:
1 2
| int? val = new int?();
val .Value = null; |
更不用说val.Value是只读属性,这意味着我们甚至不能使用如下内容:
但是,多形重载隐式转换运算符让我们可以这样做:
不必担心一词多义,不过,只要它正确就行了。:)
- "记住:可以为空的还是速记的?是一门课。"这是错误的!nullable是一个结构。它重载等于和==运算符,以便在与空值比较时返回true。编译器不适合进行这种比较。
- @andrewjs-您认为它是一个结构(而不是类)是正确的,但是您错了,它重载了==运算符。如果在VisualStudio 2013中键入Nullable和f12 it,您将看到它只重载与X和Equals(object other)方法之间的转换。但是,我认为==运算符默认使用该方法,因此效果相同。事实上,我有一段时间一直想更新这个答案,但我很懒惰和/或很忙。此评论现在必须做:)
- 我对ildasm做了一个快速检查,您对编译器进行了一些魔术的看法是正确的;将一个nullable对象与空进行比较实际上会转换为对hasValue的调用。很有趣!
- @实际上,编译器做了大量的工作来优化nullables。例如,如果将一个值赋给一个可以为空的类型,那么它实际上根本就不是一个可以为空的类型(例如,int? val = 42; val.GetType() == typeof(int))。因此,不仅可以为空的结构可以等于空,而且通常根本不可以为空!:d同样,当您对一个可以为空的值进行装箱时,您装箱的是int,而不是int?,并且当int?没有值时,您得到的是null,而不是装箱的可以为空的值。这基本上意味着正确使用nullable几乎没有开销。)
- "如果没有编译器的帮助,试图对变量被指定为空的空表调用hasValue,将产生空引用运行时错误。"—这是胡说。nullable是值类型,而不是引用类型,因此它不可能获得空引用错误。此页上的许多注释反映了这种混淆,将"nullable"与"reference type"混淆了。
- @Luaan"它实际上根本不可以为空"——这一点和您的其他评论都是胡说八道。类型可以为空;值不能为空。是否为可以为空的类型?可以是t或null。当然,当它是t时,gettype()返回type of(t),因为gettype生成运行时类型。"很少有开销——当然也有开销;这个标志需要额外的存储空间来表示值是否为空,并且需要运行时检查来测试这个标志。
- @Jimbalter好的,那么告诉我一个值类型如何在不欺骗运行时/编译器的情况下拥有不同于编译时类型的运行时类型。您能使自己的可空类型的行为方式相同吗?是否可以为值类型重载装箱运算符?你是怎么超载的?你从Nullable.GetValueOrDefault那里得到多少开销?在任何其他值类型的装箱之上装箱一个可为空的类型,您会得到什么开销?唯一的一种情况是不装箱(标志)和检查空值(这就是您想要的!).
- @Luaan具有int?编译时类型的变量具有int或null的运行时类型。它不是"欺骗",而是抽象的实现。具有Base编译时类型的变量可以将Derived作为运行时类型——不存在"欺骗"。这是非常基本的。
- @真的吗?这很有趣。那么,关于类中的一个可以为空的字段,内存分析器能告诉您什么呢?如何声明从C中的另一个值类型继承的值类型?如何声明自己的可空类型,该类型的行为与.NET的可空类型相同?从什么时候开始,null是.NET中的一个类型?你能指出clr/c规范中提到的那个部分吗?空值在clr规范中定义得很好,它们的行为不是"抽象的实现"——这是一个契约。但是如果你能做的最好的事就是寻人攻击,那就尽情享受吧。
在VB.Net。如果可以使用".hasValue",请不要使用"isnot nothing"。我刚刚解决了一个"操作可能会破坏运行时"中的信任错误,将"isnot nothing"替换为".hasValue"。我真的不明白为什么,但是编译器中发生了不同的事情。我假设"!C中的"=null"可能有相同的问题。
- 这不是一个正确的假设。
- 由于可读性,我更喜欢HasValue。IsNot Nothing是一个丑陋的表达(因为双重否定)。
- @史蒂芬"不是什么"不是双重否定。"没有"不是一个负数,它是一个离散的量,甚至在编程领域之外。从语法上讲,这个量不是零,和"这个量不是零"一模一样,也不是双负数。
- 不是我不想反对这里没有真相,而是现在就来吧。不是没有什么是明显的,嗯,过于消极的。为什么不写一些积极明确的东西,比如hasvalue?这不是语法测试,而是编码,关键目标是清晰。
- 杰姆比亚诺:我同意这不是双重否定,而是单一否定,几乎和简单的正面表达一样难看,也不那么清晰。
如果您使用LINQ并且希望保持代码简短,我建议您总是使用!=null。
这就是为什么:
假设我们有一个类Foo,它有一个可以为空的双变量SomeDouble。
1 2 3 4 5
| public class Foo
{
public double? SomeDouble;
//some other properties
} |
如果在我们的代码中的某个地方,我们希望从foo集合中获取具有非空somedouble值的所有foo(假设集合中的某些foo也可以为空),那么最后我们至少有三种方法来编写函数(如果我们使用c 6):
1 2 3 4 5 6 7
| 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
} |
在这种情况下,我建议总是选择短一点的
- 是的,foo?.SomeDouble.HasValue是编译时错误(在我的术语中不是"throw"),因为它的类型是bool?,而不仅仅是bool。(.Where方法需要一个Func)当然可以做(foo?.SomeDouble).HasValue,因为它有bool类型。这就是您的第一行被C编译器(至少是正式的)内部"翻译"成的内容。
一般的回答和经验法则:如果您有一个选项(例如,编写自定义序列化程序)来处理与object不同的管道中的可空值,并使用它们的特定属性,那么就执行该操作并使用可空的特定属性。因此,从一致的思维角度来看,应该优先选择HasValue。一致的思想可以帮助您编写更好的代码,不要在细节上花费太多时间。例如,第二种方法的效率要高出许多倍(主要是因为编译器的内联和装箱,但数字仍然很有表现力):
1 2 3 4 5 6 7 8 9
| public static bool CheckObjectImpl(object o)
{
return o != null;
}
public static bool CheckNullableImpl<T>(T? o) where T: struct
{
return o.HasValue;
} |
基准测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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 | |
基准代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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
人们说,建议"因为始终如一的思考而偏爱有价值的东西"是不相关和无用的。你能预测这个的性能吗?
1 2 3 4
| public static bool CheckNullableGenericImpl<T>(T? t) where T: struct
{
return t != null;
} |
PPS继续下降,但没有人试图预测CheckNullableGenericImpl的表现。而且编译器不会帮助您用HasValue替换!=null。如果您对性能感兴趣,可以直接使用HasValue。
- 你的CheckObjectImpl把空值放入object中,而CheckNullableImpl不使用拳击。因此,这种比较是非常不公平的。它不仅不是费用,而且也没用,因为正如公认的答案所指出的,编译器无论如何都会将!=重写为HasValue。
- 你的评论是不公平的。我只能重复一下,这是特定的情况,但在某些领域,例如在序列化程序中,这是常见的情况。此外,为了回答这个问题,我还演示了如果忽略nullable的结构性质,事情将如何变得更加复杂。读者会决定它是否无用,对我来说它是有趣的,我会分享它。
- 读者不会忽略Nullable的结构性质,您可以(通过将它装箱为object)。当应用左侧可为空的!= null时,不会发生装箱,因为对可为空的!=的支持在编译器级别起作用。当您首先将nullable装箱到一个object中,从而对编译器隐藏它时,它是不同的。原则上,无论是CheckObjectImpl(object o)还是基准测试都没有意义。
- 你期望这个结果,而你对数字的不同不感兴趣,我期望这个结果,我对数字的不同很感兴趣。为什么你要告诉我其他人应该感兴趣?你有什么问题?寻求另一个让步?同样,在通常的序列化程序中,代码主要使用装箱的值,并抽象为object类型。所以这种情况是真实的。
- 我的问题是我关心这个网站的内容质量。你所发布的内容要么是误导性的,要么是错误的。如果您试图回答OP的问题,那么您的回答完全是错误的,这很容易通过用CheckObject中的主体替换对CheckObjectImpl的调用来证明。然而,你最近的评论表明,当你决定回答这个8年前的问题时,实际上你心里有一个完全不同的问题,这使得你的回答在最初问题的背景下产生误导。这不是手术室要问的。
- 是的,我理解我的答案比问题更通用,同时比从另一个角度展示情况更具体。我同意你的评论是有道理的。除了两个瞬间:a)情况是真实的b)你不仅要评论,还要删除我的答案。其次,我只能用你希望得到的泡泡糖来解释。你真丢脸。
- 我坚信这个网站和它的成功一样,特别是因为它建立在狭隘具体问题和狭隘具体答案的原则上,这就是为什么我认为从这个问题中删除这个内容是正确的。如果你有内容要分享,这是非常欢迎的,只要在一个更合适的问题下做。如果没有一个问题,你可以自己创建一个问题并回答它。
- 把自己放在下一个用谷歌搜索what is faster != or HasValue的人的位置上。他提出这个问题,浏览你的答案,欣赏你的基准,说:"哎呀,我绝不会使用!=,因为它显然慢得多!"这是一个非常错误的结论,他将继续传播下去。这就是为什么我相信你的回答是有害的——它回答了一个错误的问题,从而在毫无戒心的读者中产生了一个错误的结论。考虑一下当你把你的CheckNullableImpl改为return o != null;时会发生什么,你会得到相同的基准结果。
- 你不同意我的回答。我的答案是针对具体情况。我不隐藏这种情况是具体的,用文本("序列化程序管道")描述这种情况,显示代码和有趣的数字。人们并不愚蠢,可以理解它,开始思考,享受他们的想法。思考是快乐的,而不是"猎徽"。
- 我在和你的答案争论。你的回答看起来像是显示了!=和HasValue之间的区别,实际上它显示了object o和T? o之间的区别。如果你按照我的建议去做,也就是把CheckNullableImpl改写成public static bool CheckNullableImpl(T? o) where T: struct { return o != null; },你会得到一个基准,它清楚地表明!=比!=慢得多。这会让你得出结论,你的答案所描述的问题根本不是关于!=与HasValue的。
- 意识到这一点后,您可以重新处理您的问题,以解释object o和T? o之间的差异实际上是不同的,不管您使用的是!=还是HasValue,所以在编写自定义序列化时,应该避免装箱和取消装箱可以为空的值。那么,这将是一个绝对正确的答案,而不是一个最容易引起误解的答案,但是,在你重新阅读它之后,你会删除它,因为它与OP的要求完全无关。