【问题标题】:How is the boxing/unboxing behavior of Nullable<T> possible?Nullable<T> 的装箱/拆箱行为如何可能?
【发布时间】:2011-04-16 02:11:16
【问题描述】:

今天早些时候我发生了一件让我摸不着头脑的事情。

Nullable&lt;T&gt; 类型的任何变量都可以分配给null。例如:

int? i = null;

起初,如果不以某种方式定义从 objectNullable&lt;T&gt; 的隐式转换,我看不到这怎么可能:

public static implicit operator Nullable<T>(object box);

但是上面的操作符显然不存在,就好像它存在一样,那么下面的也必须是合法的,至少在编译时是合法的(它不是):

int? i = new object();

然后我意识到,也许Nullable&lt;T&gt; 类型可以定义隐式转换为一些永远无法实例化的任意引用类型,如下所示:

public abstract class DummyBox
{
    private DummyBox()
    { }
}

public struct Nullable<T> where T : struct
{
    public static implicit operator Nullable<T>(DummyBox box)
    {
        if (box == null)
        {
            return new Nullable<T>();
        }

        // This should never be possible, as a DummyBox cannot be instantiated.
        throw new InvalidCastException();
    }
}

但是,这并不能解释我接下来发生的事情:如果任何 Nullable&lt;T&gt; 值的 HasValue 属性是 false,那么该值将被装箱为 null

int? i = new int?();
object x = i; // Now x is null.

此外,如果HasValuetrue那么该值将被装箱为T 而不是T?

int? i = 5;
object x = i; // Now x is a boxed int, NOT a boxed Nullable<int>.

但是 this 似乎暗示了从 Nullable&lt;T&gt;object 的自定义隐式转换:

public static implicit operator object(Nullable<T> value);

显然情况并非如此,因为object 是所有类型的基类,并且用户定义的与基类型之间的隐式转换是非法的(也应该如此)。

似乎object x = i; 应该像任何其他值类型一样将i 装箱,这样x.GetType() 将产生与typeof(int?) 相同的结果(而不是抛出NullReferenceException)。

所以我挖了一下,果然,这种行为是特定于 Nullable&lt;T&gt; 类型的,在 C# 和 VB.NET 规范中都特别定义,并且在任何用户定义的 @987654352 中都无法重现@ (C#) 或 Structure (VB.NET)。

这就是我仍然感到困惑的原因。

这种特殊的装箱和拆箱行为似乎无法手动实现。它之所以有效,是因为 C# 和 VB.NET 都对 Nullable&lt;T&gt; 类型进行了特殊处理。

  1. Nullable&lt;T&gt; 没有得到这种特殊处理的情况下,理论上是否可能存在不同的基于 CLI 的语言? Nullable&lt;T&gt; 类型不会因此在不同的语言中表现出不同的行为吗?

  2. C# 和 VB.NET 如何实现这种行为? CLR 支持吗? (也就是说,CLR 是否允许一个类型以某种方式“覆盖”它的装箱方式,即使 C# 和 VB.NET 本身禁止它?)

  3. 是否甚至可能(在 C# 或 VB.NET 中)将 Nullable&lt;T&gt; 装箱为 object

【问题讨论】:

标签: c# .net vb.net nullable boxing


【解决方案1】:

发生了两件事:

1) 编译器不将“null”视为 null reference,而是将其视为 null value...它需要转换为的任何类型的 null 值。对于Nullable&lt;T&gt;,它只是HasValue 字段/属性的值为False。因此,如果您有一个int? 类型的变量,那么该变量的值很可能是null - 您只需要稍微改变您对null 含义的理解即可。

2) 装箱可为空的类型得到 CLR 本身的特殊处理。这与您的第二个示例相关:

    int? i = new int?();
    object x = i;

编译器会将任何可空类型值与不可空类型值不同地装箱。如果该值不为空,则结果将与将相同的值装箱为不可为空的类型值相同 - 因此值为 5 的 int? 与值为 5 的 int 装箱方式相同 - “可空性”丢失了。但是,可空类型的空值被装箱到空引用,而不是创建一个对象。

这是应社区的要求在 CLR v2 周期后期引入的。

这意味着没有“装箱的可空值类型值”之类的东西。

【讨论】:

  • 像往常一样发现 - 看起来 IL 代码,对象 x = i 被转换为指示 CLR 级别支持的框指令。
  • 我明白你对拳击从 CLR 获得特殊处理的意思:我刚刚写了一个快速测试并查看了 Reflector 中的 IL,并注意到 C# 编译器似乎没有对删除Nullable&lt;T&gt; 本身的装箱。但是,Nullable&lt;T&gt; 的装箱效果在 C# 和 VB.NET 规范中是独立指定的,这不是很奇怪吗?它也在 CLI 规范中,你知道吗?
  • @Dan:是的,它在 C# 规范中。我不知道它是否在 VB.NET 规范中。在 CLI 规范 (ECMA-335) 中,它位于分区 1 的第 8.2.4 节中。
  • 我也能在 VB 规范中找到它(在第 8.6.1 节中,至少对于规范的 9.0 版)。我想我只是对为什么会在三个地方指定它感到困惑——CLI 规范,以及 C# 和 VB 规范——而不是仅仅在 CLI 规范中。
【解决方案2】:

您没看错:Nullable&lt;T&gt; 在 VB 和 C# 中都得到了编译器的特殊处理。因此:

  1. 是的。语言编译器需要特例 Nullable&lt;T&gt;
  2. 编译器重构Nullable&lt;T&gt; 的用法。运算符只是语法糖。
  3. 据我所知没有。

【讨论】:

  • 所以响应您的第二个答案:C# 编译器(例如)必须将 object x = i; 重构为 object x = i.HasValue ? (object)i.Value : null; 之类的东西,对吗?这意味着该语言实际上完全不允许标准的装箱行为(我刚刚意识到 object x = (int?)5; 使 x 成为装箱的 intnot Nullable&lt;int&gt;)。有趣...
  • 看IL,CLR对Nullable有特殊的支持。 Complier 发出 box 指令,CLR 检查 value 以决定是否将 ref 设置为 null 值或 box 实际值类型值。
  • 通过查看从 GetRandomNullable&lt;T&gt; 方法生成的 IL,我刚刚在 Reflector 中鞭打自己,看来 VinayC 是正确的:我没有看到 C# 编译器实际上重构了装箱,这意味着(我)特殊处理实际上确实发生在CLR级别。但如果这是真的,那么(对我来说)行为将在语言规范本身中定义似乎很奇怪。有什么想法吗?
  • @Dan Tao:对于拳击来说,这是真的。我的回答只涉及操作员“超载”(== null 测试、操作员起重等)。
【解决方案3】:

我问自己同样的问题,我也期待在.net Nullable source code 中有一些隐含的Nullable&lt;T&gt; 运算符,所以我查看了与int? a = null; 对应的IL 代码是什么,以了解幕后发生的事情:

c#代码:

int? a = null;
int? a2 = new int?();
object a3 = null;
int? b = 5;
int? b2 = new int?(5);

IL 代码(使用 LINQPad 5 生成):

IL_0000:  nop         
IL_0001:  ldloca.s    00 // a
IL_0003:  initobj     System.Nullable<System.Int32>
IL_0009:  ldloca.s    01 // a2
IL_000B:  initobj     System.Nullable<System.Int32>
IL_0011:  ldnull      
IL_0012:  stloc.2     // a3
IL_0013:  ldloca.s    03 // b
IL_0015:  ldc.i4.5    
IL_0016:  call        System.Nullable<System.Int32>..ctor
IL_001B:  ldloca.s    04 // b2
IL_001D:  ldc.i4.5    
IL_001E:  call        System.Nullable<System.Int32>..ctor
IL_0023:  ret   

我们看到编译器将int? a = null 更改为int? a = new int?(),这与object a3 = null 完全不同。很明显,Nullables 有一个特殊的编译器处理。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多