C# 中可空类型的替代方案

Posted

技术标签:

【中文标题】C# 中可空类型的替代方案【英文标题】:Alternatives to nullable types in C# 【发布时间】:2010-10-26 23:20:31 【问题描述】:

我正在编写处理一系列数字数据的算法,有时,该系列中的一个值需要为空。但是,由于此应用程序对性能至关重要,因此我避免使用可空类型。我已经对算法进行了性能测试,以专门比较使用可空类型和不可空类型的性能,在最好的情况下,可空类型慢 2 倍,但通常更差。

最常用的数据类型是double,目前选择的null 替代方案是double.NaN。但是我知道这不是 NaN 值的确切预期用途,所以我不确定这是否有任何我无法预见的问题以及最佳实践是什么。

我有兴趣找出以下数据类型的最佳 null 替代方案:double/float、decimal、DateTime、int/long(尽管其他数据类型非常受欢迎)

编辑:我想我需要澄清我对性能的要求。通过这些算法一次处理大量数字数据需要几个小时。因此,虽然例如 10ms 或 20ms 之间的差异通常是微不足道的,但在这种情况下,它确实会对所用时间产生重大影响。

【问题讨论】:

一些关于您期望的数据类型和数量的信息会很好。 仅供参考 - 我的测试表明,对于两个值都不为 null 的情况, Nullable 和幻数非常相等;当涉及到空值时,是的,幻数方法会更快一些……但是是否足够快(呃)值得带来不便?它仍然非常非常快 - 在我的机器上 86 毫秒(幻数)与 144 毫秒(Nullable)中的 50M 次迭代...... (另请参阅我对您的测试的回复 i == null 等;编译器已经通过“提升”运算符执行此操作;您可能会复制它...) @Marc - 我同意你关于性能是否值得的观点,我几乎总是倾向于更好的代码而不是过度优化,因为不清楚代码的成本很容易更高。抱歉,我认为我的问题可能对我的性能需求不够清楚 - 让我先编辑.. @Peterchen - 让我也添加这个 【参考方案1】:

可以通过定义自己的结构来避免与Nullable<T> 相关的一些性能下降

struct MaybeValid<T>

    public bool isValue;
    public T Value;

如果需要,可以定义构造函数,或从TMaybeValid&lt;T&gt; 的转换运算符等,但过度使用这些东西可能会产生次优性能。如果避免不必要的数据复制,暴露字段结构可能会很有效。有些人可能不赞成暴露字段的概念,但它们可以比属性更有效。如果将返回T 的函数需要有一个T 类型的变量来保存其返回值,则使用MaybeValid&lt;Foo&gt; 只需将要返回的事物的大小增加4。相比之下,使用Nullable&lt;Foo&gt; 需要函数首先计算Foo,然后将其副本传递给Nullable&lt;Foo&gt; 的构造函数。此外,返回Nullable&lt;Foo&gt; 将要求任何想要使用返回值的代码必须至少对Foo 类型的存储位置(变量或临时)进行一个额外的复制,然后才能对其进行任何有用的操作。相比之下,代码使用Foo 类型变量的Value 字段的效率与任何其他变量差不多。

【讨论】:

【参考方案2】:

我正在开发一个使用 NaN 作为null 值的大型项目。我对此并不完全满意-出于与您类似的原因:不知道会出什么问题。到目前为止,我们还没有遇到任何实际问题,但请注意以下几点:

NaN 算术 - 虽然在大多数情况下,“NaN 提升”是一件好事,但它可能并不总是如您所愿。

比较 - 如果您希望 NaN 比较相等,则值的比较会变得相当昂贵。现在,无论如何测试浮点数的相等性并不简单,但是排序 (a

代码感染 - 我看到很多算术代码需要对 NaN 进行特定处理才能正确。因此,出于性能原因,您最终会得到“接受 NaN 的函数”和“不接受 NaN 的函数”。

其他非有限值 NaN 是唯一的非有限值。应该记住...

浮点异常在禁用时不是问题。直到有人启用它们。真实故事:ActiveX 控件中 NaN 的静态初始化。听起来并不可怕,直到您将安装更改为使用 InnoSetup,它使用 Pascal/Delphi(?) 内核,默认情况下启用 FPU 异常。我花了一段时间才弄清楚。

所以,总而言之,没什么大不了的,尽管我不想经常考虑 NaN。


我会尽可能频繁地使用 Nullable 类型,除非它们(被证明是)性能/资源限制。一种情况可能是带有偶尔 NaN 的大型向量/矩阵,或大量命名的单个值其中默认 NaN 行为是正确的


或者,您可以将索引向量用于向量和矩阵、标准“稀疏矩阵”实现或单独的布尔/位向量。

【讨论】:

【参考方案3】:

调用 Nullable 的成员或属性之一(装箱)时,可能会显着降低性能。

尝试使用带有双精度 + 布尔值的结构来判断是否指定了值。

【讨论】:

这正是 Nullable 所做的——它是一个结构,它有一个值(例如,双精度类型)和一个布尔值,指示有或没有赋值。而且没有拳击开销。 属性(HasValue 和 Value)是内部方法(get_HasValue 和 get_Value)。所以它们受到装箱的影响(前提是这里没有出现 Nullable 的特殊编译器魔法)。 @stefan-mg;结构方法不需要装箱,除非该方法是虚拟的且未被覆盖(即 GetHashCode()、Equals()、ToString() 等)。【参考方案4】:

好吧,如果您排除了Nullable&lt;T&gt;,那么您将获得域值 - 即您视为 null 的幻数。虽然这不是理想,但它也并不少见——例如,许多主框架代码将DateTime.MinValue 视为null。这至少使伤害远离常见值...

编辑以仅在没有 NaN 的地方突出显示

所以在没有NaN 的地方,也许可以使用.MinValue - 但只要记住如果你不小心使用相同的值意味着相同的数字会发生什么...

显然,对于未签名的数据,您需要.MaxValue(避免零!!!)。

就个人而言,我会尝试使用Nullable&lt;T&gt; 来更安全地表达我的意图……也许有办法优化您的Nullable&lt;T&gt; 代码。而且 - 当您在所有需要的地方检查了幻数时,也许它不会比 Nullable&lt;T&gt; 快多少?

【讨论】:

我同意,我认为这是一个比双打更好的选择,除非你必须让 long.MaxValue 有效。 对于双精度值或浮点值,如果您不需要它们,NaN 或无穷大之一可能会用作“空”值。 关于检查,Null 类型需要相同数量的检查,我检查一个幻数,我检查 null。所以我执行的性能测试确实考虑到了这一点。我同意这并不理想,但在这种情况下,性能不理想。 1 优先级。而在这种场景下,像 int + int 和 int 这样简单的操作之间的性能差异呢? + 整数?很重要。 我刚刚对 (i == int.MinValue || j == int.MinValue) 进行了快速简单的性能测试? int.MinValue : (i+j);与可空的 i+j 相比,令我惊讶的是可空的速度慢了约 3 倍! 性能可能因类型而异;我得到 ~2 使用 int... 重新明确检查:一旦你知道某物有一个值(通过 !=null.HasValue),使用 GetValueOrDefault()(不是 Value 或演员) - 这是最快的路线。 【参考方案5】:

部分答案:

Float 和 Double 提供 NaN(不是数字)。 NaN 有点棘手,因为根据规范,NaN != NaN。如果您想知道一个数字是否为 NaN,则需要使用 Double.IsNaN()。

另见Binary floating point and .NET。

【讨论】:

顺便... >.【参考方案6】:

我有点不同意 Gravell 在这个特定的边缘情况:一个 Null-ed 变量被认为是“未定义”,它没有值。因此,无论使用什么来表示都可以:即使是幻数,但是对于幻数,您必须考虑到幻数在将来突然变成“有效”值时总会困扰您。使用 Double.NaN,您不必为此担心:它永远不会成为有效的替身。虽然,您必须考虑到双打序列意义上的 NaN 只能用作“未定义”的标记,但显然,您也不能将其用作序列中的错误代码。

因此,无论使用什么来标记“未定义”:必须在值集的上下文中明确该特定值被认为是“未定义”的值,并且将来不会改变。

如果 Nullable 给您带来太多麻烦,请使用 NaN 或其他任何方法,只要您考虑后果:选择的值表示“未定义”并且将保留。

【讨论】:

你是对的,我一直不清楚。我只指没有 NaN 的 MinValue 等 - int、long、decimal、DateTime 等。对于 double/float,NaN 是显而易见的答案(我从问题中假设)。

以上是关于C# 中可空类型的替代方案的主要内容,如果未能解决你的问题,请参考以下文章

c# 中的可空类型是啥?

嵌套js代码中可空值的可空数组首先graphql

[译] Dart中可空性语法的定案: a?[b] 或 a?.[b]

C#可空类型(Nullable)

C# 8中的可空引用类型

了解下C# 可空类型(Nullable)