关于c#:Nullable<>的装箱/拆箱行为如何?

How is the boxing/unboxing behavior of Nullable<T> possible?

今天早些时候我刚刚发生的事情让我抓耳挠腮。

任何Nullable类型的变量都可以分配给null。例如:

1
int? i = null;

起初,我不知道如果不以某种方式定义从objectNullable的隐式转换,这是怎么可能的:

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

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

1
int? i = new object();

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
    }
}

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

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

此外,如果HasValuetrue,则该值将作为T而不是T?装箱:

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

但这似乎意味着有一种从Nullableobject的隐式转换:

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

显然情况并非如此,因为object是所有类型的基类,用户定义的隐式转换到/从基类型转换是非法的(以及它们应该是非法的)。

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

所以我仔细研究了一下,可以肯定的是,这个行为是针对Nullable类型的,在c和vb.net规范中都有专门的定义,在任何用户定义的structStructure中都不可复制。

这就是为什么我仍然困惑的原因。

这种特殊的装箱和拆箱行为似乎不可能用手工实现。它只起作用,因为C和vb.net都对Nullable类型进行了特殊处理。

  • 理论上不可能存在一种不同的基于cli的语言,而Nullable没有得到这种特殊的处理吗?因此,Nullable类型在不同的语言中不会表现出不同的行为吗?

  • C和vb.net如何实现这种行为?CLR支持它吗?(也就是说,clr是否允许类型以某种方式"重写"装箱的方式,即使c和vb.net本身禁止这样做?)

  • 是否有可能(在c或vb.net中)将Nullable装箱为object


  • 有两件事发生了:

    1)编译器不将"空"视为空引用,而是将其视为空值…需要转换为的任何类型的空值。在Nullable的情况下,它只是HasValue字段/属性的值为假。所以如果你有一个int?类型的变量,这个变量的值很可能是null,你只需要改变你对null的理解一点。

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

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

    编译器将以不同于不可为空的类型值的方式对任何可为空的类型值进行装箱。如果值不为空,则结果将与将值与不可为空的类型值装箱相同-因此,值为5的int?与值为5的int装箱的方式相同-"可空性"将丢失。但是,可以为空的类型的空值仅限于空引用,而不是创建对象。

    这是在clr v2周期的后期,应社区的请求而引入的。

    它意味着没有"装箱的可空值类型值"这样的东西。


    你说得对:Nullable在vb和c中都得到了编译器的特殊处理。因此:

  • 对。语言编译器需要特殊情况Nullable
  • 编译器重构Nullable的用法。算符只是句法上的糖分。
  • 我不知道。

  • 我问自己同样的问题,我也希望在.NET可空源代码中有一些用于Nullable的隐式运算符,所以我查看了与int? a = null;对应的IL代码是什么,以了解在场景后面发生了什么:

    C代码:

    1
    2
    3
    4
    5
    int? a = null;
    int? a2 = new int?();
    object a3 = null;
    int? b = 5;
    int? b2 = new int?(5);

    IL代码(用LinqPad 5生成):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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非常不同。所以很明显,空值具有特殊的编译器处理。