【问题标题】:Boxing / Unboxing Nullable Types - Why this implementation?Boxing / Unboxing Nullable Types - 为什么这个实现?
【发布时间】:2009-09-07 04:51:36
【问题描述】:

通过 C# 从 CLR 中提取关于装箱/拆箱值类型 ...

装箱:如果可空实例不是null,CLR 会从可空实例中取出值并将其装箱。换句话说,一个值为 5Nullable 被装箱成一个值为 5 的 boxed-Int32

关于拆箱:拆箱只是获取对已装箱对象的未装箱部分的引用的行为。问题是装箱的值类型不能简单地拆箱为该值类型的可为空版本,因为装箱的值中没有 boolean hasValue 字段。因此,当将值类型拆箱为可空版本时,CLR 必须分配一个 Nullable 对象,将 hasValue 字段初始化为 true ,并将 value 字段设置为与装箱值类型相同的值。这会影响您的应用程序性能(拆箱期间的内存分配)。

为什么 CLR 团队在 Nullable 类型上遇到这么多麻烦?为什么一开始不简单地将它装箱成 Nullable

【问题讨论】:

  • "拆箱时的内存分配" 什么???这本书在这方面肯定是错误的。
  • @BenVoigt 拆箱成可空类型比拆箱成普通类型慢大约 20 倍,但只有在常规转换时,as 运算符才不会比常规拆箱慢。跨度>

标签: c# .net clr nullable


【解决方案1】:

我记得这种行为是最后一刻才做出的改变。在 .NET 2.0 的早期测试版中,Nullable<T> 是一种“正常”值类型。装箱 nullint? 将其变成带有布尔标志的装箱 int?。我认为他们决定选择当前方法的原因是一致性。说:

int? test = null;
object obj = test;
if (test != null)
   Console.WriteLine("test is not null");
if (obj != null)
   Console.WriteLine("obj is not null"); 

在前一种方法中(框null -> 装箱Nullable<T>),你不会得到“测试不为空”,但你会得到“对象不为空”,这很奇怪。

此外,如果他们将可空值装箱为 boxed-Nullable<T>

int? val = 42;
object obj = val;

if (obj != null) {
   // Our object is not null, so intuitively it's an `int` value:
   int x = (int)obj; // ...but this would have failed. 
}

除此之外,我相信当前的行为对于可以为空的数据库值之类的场景非常有意义(想想 SQL-CLR...)


澄清:

提供可空类型的全部意义在于使处理没有有意义值的变量变得容易。他们不想提供两种截然不同的、不相关的类型。 int? 的行为应该或多或少像一个简单的int。这就是 C# 提供提升运算符的原因。

因此,当将值类型拆箱为可空版本时,CLR 必须分配一个Nullable<T> 对象,将 hasValue 字段初始化为 true,并将值字段设置为与装箱值类型中的值相同的值。这会影响您的应用程序性能(拆箱期间的内存分配)。

这不是真的。 CLR 必须分配内存在堆栈上 来保存变量,无论它是否可以为空。为额外的布尔变量分配空间没有性能问题。

【讨论】:

  • 据我了解,在当前实现(步骤 1)中,如果 test 为空,CLR 不会装箱并返回空。 (第 2 步)如果可为空的实例不为空,则将其装箱到装箱的 int32 中。第一步不是解决“obj is not null”的问题吗?为什么他们必须执行第 2 步?抱歉,我好像漏掉了什么。
  • Preets:你的意思是他们会将null 框为空引用并将int? x = 4; 框为boxed-Nullable<int>
  • Preets:如果他们这样做了,你就不能直接把它拆箱到int
  • 有什么意义呢?将非 null 可空对象装箱为 Nullable<...> 只是在浪费一个布尔值,因此(略微)增加了 GC 压力并无缘无故地减少了处理器缓存。 Nullable<...> 背后的整个想法是,它表示恰好能够为 null 的值类型 - 但对于本来可以为 null 的装箱值而言,整个额外步骤是不必要的。
  • 虽然我可以理解处理器缓存参数(我认为大多数情况下并不重要),但我不确定 GC 压力参数。无论您是 box and int 还是 box 一个 Nullable,GC 仍然将其作为单个块处理。创建它只是一个分配(使用 GC 几乎是免费的),删除意味着标记/扫描/压缩仍然会标记、扫描和压缩一个块,无论如何。我看不出 GC 负载有任何差异……只是分配块大小的差异。我认为问题的症结在于 Mehrdad 所说的:“直接拆箱到 int”。
【解决方案2】:

我认为将空值装箱到空引用是有意义的。有一个盒装值说“我知道如果我有一个值,我会成为Int32,但我没有”对我来说似乎不直观。最好从“非值”的值类型版本(HasValue 为 false 的值)转换为“非值”的引用类型版本(空引用)。

我相信这个改变是根据社区的反馈做出的,顺便说一句。

这也允许as 有趣地用于值类型:

object mightBeADouble = GetMyValue();

double? unboxed = mightBeADouble as double?;
if (unboxed != null)
{
    ...
}

这与使用引用类型处理“不确定转换”的方式比以前更一致:

object mightBeADouble = GetMyValue();

if (mightBeADouble is double)
{
    double unboxed = (double) mightBeADouble;
    ...
}

(它也可能表现更好,因为只有一次执行时间类型检查。)

【讨论】:

  • 我知道的Nullable&lt;T&gt; 的正常使用是在函数可能有也可能没有T 要返回的情况下。从概念上讲,当T 是引用类型或可空类型时,该模式与不可空值类型一样适用。如果有Nullable&lt;int&gt; 的集合,TryGetXXX 方法的逻辑返回类型将是Nullable&lt;Nullable&lt;int&gt;``. If the return's HasValue` 为假,这意味着它无法获得Nullable&lt;int&gt;。如果外部为真,则返回值是存储在集合中的任何内容(可能为 null)。
【解决方案3】:

您通过这种行为获得的一个好处是,盒装版本实现了底层类型支持的所有接口。 (目标是使Nullable&lt;int&gt; 看起来与int 相同,以实现所有实际目的。)装箱到boxed-Nullable&lt;int&gt; 而不是boxed-int 可以防止这种行为。

来自MSDN Page,

double? d = 44.4;
  object iBoxed = d;
  // Access IConvertible interface implemented by double.
  IConvertible ic = (IConvertible)iBoxed;
  int i = ic.ToInt32(null);
  string str = ic.ToString();

Nullable&lt;int&gt; 的盒装版本中获取 int 也很简单 - 通常您不能将其拆箱为原始 src 类型以外的类型。

float f = 1.5f;
object boxed_float = f;
int int_value = (int) boxed_float; // will blow up. Cannot unbox a float to an int, you *must* unbox to a float first.

float? nullableFloat = 1.4f;
boxed_float = nullableFloat;
float fValue = (float) boxed_float;  // can unbox a float? to a float    Console.WriteLine(fValue);

在这里,您不必知道原始版本是 int 还是 Nullable 版本。 (+ 你也得到了一些性能;节省了将 hasValue 布尔值存储在盒装对象中的空间)

【讨论】:

  • 我可以将其理解为一个基本原理,但我认为最好使用一些 CLR 魔法来为 Nullable 创建包装器方法以实现 T 的接口,或者限制接口转换的奇怪装箱行为。事实上,Nullable 最终陷入了一个非常奇怪的边缘。
【解决方案4】:

我想这基本上就是它的作用。给出的描述包括您的建议(即装箱为Nullable&lt;T&gt;)。

额外的是它在装箱后设置hasValue字段。

【讨论】:

    【解决方案5】:

    我会假设该行为的原因源于 Object.Equals 的行为,最值得注意的是,如果第一个对象为 null 而第二个对象不是,则 Object.Equals 返回 false 而不是调用 Equals 方法在第二个对象上。

    如果 Object.Equals 会在第一个对象为 null 而第二个对象不是 null 的情况下调用第二个对象的 Equals 方法,那么一个为 null 值 Nullable 的对象在比较时可能会返回 True为空。就个人而言,我认为适当的补救措施是使 Nullable 的 HasValue 属性与空引用的概念无关。关于在堆上存储布尔标志所涉及的开销,可以规定对于每种类型 Nullable 都有一个静态装箱空版本,然后提供拆箱静态装箱空副本将产生一个空 Nullable,拆箱任何其他实例都会产生一个填充的实例。

    【讨论】:

      猜你喜欢
      • 2019-03-26
      • 1970-01-01
      • 2010-10-17
      • 2018-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多