我总是使用 Nullable<>.HasValue
,因为我喜欢它的语义。但是,最近我正在研究其他人的现有代码库,他们只使用 Nullable<> != null
代替。
是否有理由使用其中一个,还是纯粹是偏好?
诠释?一个; if (a.HasValue) // ...
对比
诠释?乙; if (b != null) // ...
HasValue
,因为我认为文字往往比符号更具可读性。不过,这完全取决于您,以及适合您现有风格的方式。
.HasValue
更有意义,因为它表示类型是 T?
类型,而不是可以为空的类型,例如字符串。
编译器将 null
比较替换为对 HasValue
的调用,因此没有真正的区别。只做对您和您的同事更具可读性/更有意义的那个。
我更喜欢 (a != null)
以便语法匹配引用类型。
Nullable<>
不是 引用类型。
.HasValue
一旦不再显式 .HasValue
就会变成不正确的语法2},这可能不是一种常见的情况,但是如果你曾经为了 Tuple 编写过一个结构,然后把它变成一个类,那么你就已经进入了适用的领域,并且随着 NullableRefs 的出现,这将变得更有可能发生。
我通过使用不同的方法将值分配给可为空的 int 对此进行了一些研究。这是我做各种事情时发生的事情。应该澄清发生了什么。请记住:Nullable<something>
或简写 something?
是一个结构,编译器似乎为此做了很多工作,让我们可以将它与 null 一起使用,就像它是一个类一样。
您将在下面看到、SomeNullable == null
和 SomeNullable.HasValue
将始终返回预期的真或假。尽管下面没有演示,但 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 传递给构造函数,会产生编译时错误,因为可空对象的 VALUE 不可为空。只有包装对象本身可以等于 null。
同样,我们会从以下位置得到一个编译时错误:
int? val = new int?();
val.Value = null;
更不用说 val.Value
无论如何都是只读属性,这意味着我们甚至不能使用类似的东西:
val.Value = 3;
但同样,多态重载隐式转换运算符让我们这样做:
val = 3;
无需担心 polysomthing whatchamacallits 不过,只要它工作正常? :)
Nullable<X>
并按 F12,您将看到它只会重载与 X
和 Equals(object other)
方法的转换。但是,我认为 == 运算符默认使用该方法,因此效果是一样的。我实际上一直想更新这个关于这个事实的答案一段时间,但我很懒和/或很忙。这个评论现在必须做:)
int? val = 42; val.GetType() == typeof(int)
)。因此,不仅可以为 null 的结构可以等于 null,而且通常根本不是可以为 null 的! :D 同样,当您装箱一个可空值时,您装箱的是 int
,而不是 int?
- 当 int?
没有值时,您会得到 null
而不是装箱的可空值.这基本上意味着正确使用可空值很少有任何开销:)
Null
是 .NET 中的一种类型?您能否指出 CLR/C# 规范中所说的部分?可空对象在 CLR 规范中得到了很好的定义,它们的行为不是“抽象的实现”——它是一个契约。但是,如果您能做的最好的事情就是人身攻击,那就尽情享受吧。
在 VB.Net 中,当您可以使用 .HasValue
时不要使用 IsNot Nothing
。我刚刚通过将 IsNot Nothing
替换为 .HasValue
在一个地方解决了“操作可能会破坏运行时的稳定性”中等信任错误。我真的不明白为什么,但是编译器中发生的事情有所不同。我假设 C# 中的 != null
可能有同样的问题。
HasValue
。 IsNot Nothing
确实是一个丑陋的表达式(因为双重否定)。
如果您使用 linq 并希望保持代码简短,我建议您始终使用 !=null
这就是为什么:
假设我们有一个类 Foo
,其中有一个 可为空的 double 变量 SomeDouble
public class Foo
{
public double? SomeDouble;
//some other properties
}
如果在我们的代码中的某个地方,我们想从 Foo 集合中获取所有具有非 null SomeDouble 值的 Foo(假设集合中的某些 foos 也可以为 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# 编译器在内部将第一行“翻译”成的内容(至少形式上如此)。
第二种方法会有效很多倍(主要是因为编译器内联和装箱,但数字仍然很有表现力):
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
不使用装箱。因此,比较是非常不公平的。它不仅不是票价,而且也是无用的,因为如 accepted answer 中所述,编译器无论如何都会将 !=
重写为 HasValue
。
Nullable<T>
的结构性质,你会(通过将其装箱到 object
中)。当您在左侧应用带有可为空值的 != null
时,不会发生装箱,因为对可空值的 !=
的支持在编译器级别起作用。当您通过首先将它装箱到 object
中来隐藏编译器的可空值时,情况会有所不同。 CheckObjectImpl(object o)
和您的基准在原则上都没有意义。
CheckObjectImpl
的 body 在 { 2}。但是,您最近的评论表明,当您决定回答这个 8 年前的问题时,您实际上有一个完全不同的问题,这使您的回答在原始问题的上下文中具有误导性。这不是 OP 所要求的。
what is faster != or HasValue
的人着想。他遇到了这个问题,浏览了您的答案,赞赏您的基准并说:“天啊,我永远不会使用 !=
,因为它显然要慢得多!”这是一个非常错误的结论,然后他将继续传播。这就是为什么我认为你的回答是有害的——它回答了一个错误的问题,从而在毫无戒心的读者中植入了一个错误的结论。考虑一下当您将 CheckNullableImpl
更改为 also 时会发生什么return o != null;
您将获得相同的基准测试结果。
!=
和 HasValue
之间的区别,但实际上它显示了 object o
和 T? o
之间的区别。如果您按照我的建议进行操作,即将 CheckNullableImpl
重写为 public static bool CheckNullableImpl<T>(T? o) where T: struct { return o != null; }
,您最终会得到一个基准,清楚地表明 !=
比 !=
慢得多。这应该使您得出结论,您的答案描述的问题根本与 !=
与 HasValue
无关。
int? x = null
给我一种可以为空的实例是引用类型的错觉。但事实是 Nullable<T>是一个值类型。感觉我会得到一个 NullReferenceException 来做:int? x = null; Use(x.HasValue)
。Nullable<int>
而不是int?
。