我一直使用Nullable<>.HasValue
,因为我喜欢这个语义。然而,最近我在别人的现有代码库上工作时,他们专门使用Nullable<> != null
。
使用其中一个的原因是什么,还是纯粹个人偏好?
int? a; if (a.HasValue) // ...
对比。
int? b; if (b != null) // ...
我一直使用Nullable<>.HasValue
,因为我喜欢这个语义。然而,最近我在别人的现有代码库上工作时,他们专门使用Nullable<> != null
。
使用其中一个的原因是什么,还是纯粹个人偏好?
int? a;
if (a.HasValue)
// ...
对比。
int? b;
if (b != null)
// ...
编译器会将null
比较替换为调用HasValue
,因此实际上没有区别。只需选择更易读/更合理的方式即可,以适应您和同事们的习惯。
int? x = null
会让我产生可空实例是引用类型的错觉。但事实上,Nullable<T> 是值类型。如果这样做:int? x = null; Use(x.HasValue)
,我会感觉会收到一个 NullReferenceException 异常。 - KFLNullable<int>
代替 int?
。 - Cole Tobin我更喜欢使用(a != null)
,这样语法就与引用类型匹配。
Nullable<>
并不是引用类型。 - Luaan.HasValue
则会成为不正确的语法,一旦它不再明确为“Nullable”,这可能不是常见情况,但如果您曾经为了Tuple而编写了一个结构体,然后将其转换为类,则已经处于适用此设置的区域,并且随着NullableRefs的出现,这种情况将变得更加普遍。 - Captain Prinny我通过使用不同的方法为可空int类型分配值进行了一些研究。以下是当我执行各种操作时发生的情况。应该澄清正在发生的事情。
请记住:Nullable<something>
或简写something?
是一个结构体,编译器似乎在做很多工作,让我们像使用类一样使用null。
如下所示,SomeNullable == null
和SomeNullable.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;
Nullable<X>
并按F12键,你会发现它只重载了转换为和从X
的方法以及Equals(object other)
方法。然而,我认为==运算符默认使用该方法,所以效果是相同的。实际上,我一直想更新这个答案的事实,但我懒得或者太忙了。这个评论暂时就这样吧 :) - Perrin Larsonint? val = 42; val.GetType() == typeof(int)
)。因此,可空结构既可以等于null,也经常根本不是可为空!:D 同样地,当对可空值进行装箱时,你将装箱为int
而不是int?
- 当int?
没有值时,你会得到null
而不是装箱的可空值。这基本上意味着使用可空类型几乎没有开销 :) - LuaanNull
是.NET中的一种类型?你能指出CLR/C#规范中说到这一点的部分吗?可空类型在CLR规范中有明确定义,它们的行为不是“抽象的实现”,而是契约。但如果你最好的办法只是进行人身攻击,那就尽情享受吧。 - Luaan.HasValue
,请勿使用IsNot Nothing
。我刚刚通过在一个位置上用.HasValue
替换IsNot Nothing
解决了一个“操作可能破坏运行时”的中等信任错误。我不是很理解为什么会这样,但编译器中发生了一些不同的事情。我会假设在C#中!= null
可能会有相同的问题。HasValue
。IsNot Nothing
是一个很丑陋的表达式(因为有双重否定)。 - Stefan Steinegger!= 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
}
foo?.SomeDouble.HasValue
是一个编译时错误(在我的术语中不是“抛出异常”),因为它的类型是 bool?
,而不仅仅是 bool
。(.Where
方法需要一个 Func<Foo, bool>
。)当然,做 (foo?.SomeDouble).HasValue
是允许的,因为它的类型是 bool
。这就是你的第一行代码被 C# 编译器内部“翻译”成的形式(至少是正式的)。 - Jeppe Stig Nielsen第二种方法将会更加有效(主要因为编译器的内联和装箱,但数字仍然非常具有表现力):
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
。
CheckObjectImpl
boxes 可空值为 object
,而 CheckNullableImpl
则不使用装箱。因此比较是非常不公平的。它不仅不公平,而且也是没有用的,因为正如被接受的答案中所指出的那样,编译器会将 !=
重写为 HasValue
。 - GSergNullable<T>
的结构特性,但是您通过将其装箱为 object
来忽略了它。当您在可空类型的左侧应用 != null
时,由于可空类型对 !=
的支持在编译器级别工作,所以不会发生装箱操作。但是,当您先将可空类型装箱为 object
后再隐藏它时,情况就不同了。从原则上讲,CheckObjectImpl(object o)
和您的基准测试都没有意义。 - GSergCheckObject
中替换调用CheckObjectImpl
的方法体来轻松证明。然而,你最近的评论揭示了一个完全不同的问题,当你决定回答这个8年前的问题时,这使得你的答案在原始问题的背景下具有误导性。这不是提问者所问的。 - GSerg!=
和HasValue
之间的区别,但实际上它显示了object o
和T? o
之间的区别。如果按照我所建议的,重新编写CheckNullableImpl
成为public static bool CheckNullableImpl<T>(T? o) where T: struct { return o != null; }
,你最终会得到一个清晰地显示!=
比HasValue
更慢的基准测试结果。这应该让你得出结论,你的回答描述的问题根本不是关于!=
与HasValue
之间的问题。 - GSerg