Nullable<T>之间有什么区别?HasValue或Nullable<T>! =零?

我总是使用Nullable<>.HasValue,因为我喜欢它的语义。然而,最近我正在研究别人现有的代码库,他们专门使用Nullable<> != null

有理由使用其中一种而不是另一种吗,还是纯粹的偏好?

    <李> < pre > <代码> int ?一个; 如果(a.HasValue) / /…… < /代码> < / pre > < /李>

vs。

    <李> < pre > <代码> int ?b; If (b != null) / /…… < /代码> < / pre > < /李>
122209 次浏览

编译器将null比较替换为调用HasValue,因此没有真正的区别。选择对你和你的同事来说更有可读性/更有意义的就行。

我更喜欢(a != null),以便语法匹配引用类型。

在VB。Net,当你可以使用.HasValue时,不要使用IsNot Nothing。我刚刚解决了一个“操作可能破坏运行时间”;在一个位置用.HasValue替换IsNot Nothing产生中等信任错误。我不太明白为什么,但是在编译器中发生了一些不同的事情。我会假设c#中的!= null可能有同样的问题。

我对此做了一些研究,使用不同的方法将值赋给一个可空的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传递给构造函数,将会产生COMPILE时错误,因为可空对象的VALUE不可空。只有包装器对象本身可以等于null。

同样地,我们会从下面得到一个编译时错误:

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

更不用说val.Value是一个只读属性,这意味着我们甚至不能使用这样的属性:

val.Value = 3;

但同样,多态重载隐式转换操作符让我们做到:

val = 3;

没有必要担心多物什么的,只要它工作对吗?:)

第二种方法会有效很多倍(主要是因为编译器内联和装箱,但数字仍然非常有表现力):

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

如果你使用linq并且想要保持你的代码简短,我建议总是使用!=null

原因如下:

假设我们有一个类Foo,它有一个nullable双变量SomeDouble

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

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

在这种情况下,我建议总是选择较短的那条