如何确定运行时对象是否为可空值类型

3
首先声明:本文与如何检查对象是否可为空不是重复的问题。或者说,那个问题并没有提供有用的答案,作者进一步阐述实际上是在问如何确定给定类型(例如从MethodInfo.ReturnType返回的类型)是否可为空。
然而,这很容易。难点在于确定在编译时未知类型的运行时对象是否为可空类型。请考虑以下情况:
public void Foo(object obj)
{
    var bar = IsNullable(obj);
}

private bool IsNullable(object obj)
{
    var type = obj.GetType();
    return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
    // Alternatively something like:
    // return !(Nullable.GetUnderlyingType(type) != null);
}

这不按预期工作,因为GetType()调用会导致装箱操作(https://msdn.microsoft.com/zh-cn/library/ms366789.aspx),并返回基础值类型而非可空类型。因此,IsNullable()总是返回false。

现在,以下技巧使用类型参数推断来获取(未装箱的)类型:
private bool IsNullable<T>(T obj)
{
    var type = typeof(T);
    return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}

这看起来一开始很有前途。然而,类型参数推断仅在编译时已知对象类型时才起作用。所以:

public void Foo(object obj)
{
    int? genericObj = 23;

    var bar1 = IsNullable(genericObj);   // Works
    var bar2 = IsNullable(obj);          // Doesn't work (type parameter is Object)
}

总之,问题不在于确定类型是否可为空,而是首先获取该类型。 因此,我的挑战是:如何确定运行时对象(上述示例中的obj参数)是否可为空?给我惊喜吧 :)

9
一旦你得到了“对象”作为值,那么该值已经被装箱了。你需要去掉这个装箱操作。不幸的是,我们不知道这里的上下文,这使得回答变得困难... 如果你只有一个类型为“对象”的变量,那么你已经失去了相关信息。 - Jon Skeet
1个回答

2
抱歉,你来晚了。现在一个装箱值不再具有你所需的类型信息——将int?值装箱会导致null或装箱的int。从某种角度上讲,它类似于在null上调用GetType——这并没有实际意义,没有类型信息。
如果可以的话,尽可能使用泛型方法而不是装箱(对于一些实际可空值和对象之间的接口,dynamic非常有帮助)。如果不能,则必须使用自己的“装箱”——甚至只需创建自己的类似Nullable的类型,它将是一个类而不是非常hacky的结构体:D
如果需要,事实上,您甚至可以将Nullable类型作为结构体。破坏类型信息的不是结构体本身,而是CLR hack,使得Nullable能够与非可空值匹配的性能。这非常聪明且非常有用,但它破坏了一些基于反射的hack(比如你正在尝试的这个hack)。
这个可以正常工作:
struct MyNullable<T>
{
  private bool hasValue;
  private T value;

  public static MyNullable<T> FromValue(T value) 
  { 
    return new MyNullable<T>() { hasValue = true, value = value }; 
  }

  public static implicit operator T (MyNullable<T> n) 
  {
    return n.value;
  }
}

private bool IsMyNullable(object obj)
{
    if (obj == null) return true; // Duh

    var type = obj.GetType().Dump();
    return type.IsGenericType 
           && type.GetGenericTypeDefinition() == typeof(MyNullable<>);
}

使用System.Nullable并不一样;即使只是使用new int?(42).GetType(),你得到的结果是System.Int32而不是System.Nullable<System.Int32>System.Nullable并不是一个真正的类型——它会被运行时特殊处理。它会执行一些你自己的类型无法复制的操作,因为这个技巧甚至不在IL中的类型定义中,而是直接在CLR中实现的。另一个很好的抽象泄露就是可空类型不被认为是一个struct——如果你的泛型类型约束是struct,你不能使用可空类型。为什么?因为添加这个约束意味着可以使用例如new T?(),否则将无法使用,因为你不能创建可空的可空。使用我在这里编写的MyNullable类型很容易,但使用System.Nullable却不容易。
编辑:
CLI规范的相关部分(1.8.2.4-装箱和拆箱值):
所有值类型都有一个名为box的操作。对任何值类型进行装箱都会产生其装箱值;即,包含原始值的位拷贝的相应装箱类型的值。如果值类型是可空类型——定义为System.Nullable值类型的实例——则结果是一个空引用或类型为T的其Value属性的位拷贝,具体取决于它的HasValue属性(分别为false和true)。所有装箱类型都有一个名为unbox的操作,它会产生一个指向值的位表示的托管指针。
因此,根据定义,在可空类型上执行box操作只会生成一个空引用或存储的值,而不是“装箱可空”。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接