为什么 Nullable<T> 被 PropertyInfo.SetValue 特殊对待

Why Nullable<T> is treated special by PropertyInfo.SetValue

在实现类似于 Nullable<T> 的结构时,我发现 PropertyInfo.SetValue 对待 Nullable 类型的方式与其他类型不同。 对于 Nullable 属性,它可以设置基础类型的值

foo.GetType().GetProperty("NullableBool").SetValue(foo, true);

但对于自定义类型它抛出

System.ArgumentException: Object of type 'SomeType' cannot be converted to type NullableCase.CopyOfNullable 1[SomeType]

即使所有转换运算符都以与原始方式相同的方式被覆盖 Nullable<T>

重现代码:

   using System;

namespace NullableCase
{
    /// <summary>
    /// Copy of Nullable from .Net source code 
    /// without unrelated methodts for brevity
    /// </summary>    
    public struct CopyOfNullable<T> where T : struct
    {
        private bool hasValue;
        internal T value;

        public CopyOfNullable(T value)
        {
            this.value = value;
            this.hasValue = true;
        }

        public bool HasValue
        {            
            get
            {
                return hasValue;
            }
        }

        public T Value
        {
            get
            {
                if (!hasValue)
                {
                    throw new InvalidOperationException();
                }
                return value;
            }
        }

        public static implicit operator CopyOfNullable<T>(T value)
        {
            return new CopyOfNullable<T>(value);
        }

        public static explicit operator T(CopyOfNullable<T> value)
        {
            return value.Value;
        }

    }


    class Foo
    {
        public Nullable<bool> NullableBool { get; set; }
        public CopyOfNullable<bool> CopyOfNullablBool { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Foo foo = new Foo();

            foo.GetType().GetProperty("NullableBool").SetValue(foo, true);
            foo.GetType().GetProperty("CopyOfNullablBool").SetValue(foo, true); //here we get ArgumentException 
        }
    }
}

为什么 PropertyInfo.SetValue 对于 CopyOfNullable 类型失败而对于 Nullable<T> 通过?

Nullable<T> 在 CLR 类型系统中有特殊支持自动从 T.

转换

其实不可能有Nullable<T>的盒装实例;可空值框到基础值或实际空值。

这是BCL中为数不多的魔法类型之一;无法复制。

调用.SetValue()时,调用树如下:

  • System.Reflection.RuntimePropertyInfo.SetValue(对象对象, 对象 值,对象 [] 索引)
  • System.Reflection.RuntimePropertyInfo.SetValue(对象对象, 对象 值,BindingFlags invokeAttr,Binder 活页夹,Object[] 索引, CultureInfo 文化)
  • System.Reflection.RuntimeMethodInfo.Invoke(对象对象, BindingFlags invokeAttr、Binder 绑定器、Object[] 参数、CultureInfo 文化)
  • System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(对象对象, BindingFlags invokeAttr,Binder绑定器,Object[]参数, CultureInfo 文化)
  • System.Reflection.MethodBase.CheckArguments(对象[]参数, Binder 活页夹、BindingFlags invokeAttr、CultureInfo 文化、签名 sig)
  • System.RuntimeType.CheckValue(对象值,Binder 活页夹,CultureInfo 文化,BindingFlags invokeAttr)
  • System.RuntimeType.TryChangeType(对象值, Binder 绑定器, CultureInfo 文化,布尔值需要 SpecialCast) [仅在您使用自定义类型时调用]

不幸的是,当调用树到达 RuntimeType.CheckValue 时,它会检查对象是否是该类型的实例(在本例中为 Bool)。

    RuntimeType runtimeType;
        if (this.IsInstanceOfType(value))
        {
            Type type = null;
            RealProxy realProxy = RemotingServices.GetRealProxy(value);
            type = (realProxy == null ? value.GetType() : realProxy.GetProxiedType());
            if (type == this || !RuntimeTypeHandle.IsValueType(this))
            {
                return value;
            }
            return RuntimeType.AllocateValueType(this, value, true);
        }
        if (!base.IsByRef)
        {
            if (value == null)
            {
                return value;
            }
            if (this == RuntimeType.s_typedRef)
            {
                return value;
            }
        }

Nullable 将作为 IsInstanceOfType(value) returns true 通过逻辑检查,框架将调用 RemotingServices.GetRealProxy,这将允许该方法根据泛型类型值确定相等性。

正如我们所知,可空类型是特殊的并且有额外的语言支持(想想如何使用 int? 而不是 Nullable<int>)。当您的自定义类型遍历此相等性检查时,它不会被视为相等实例,而是继续向下到逻辑树,将其视为单独的类型,并调用 System.RuntimeType.TryChangeType

如果我们在 IsInstanceOfType 中进一步研究源代码,我们会发现 RuntimeTypeHandle.CanCastTo 用于确定相等性,并将类型委托给 VM(在本例中为 Nullable类型基于版本被烘焙到 VM 中,因为框架中的 Nullable 被装饰为 [System.Runtime.Versioning.NonVersionable])

   // For runtime type, let the VM decide.
        if (fromType != null)
        {
            // both this and c (or their underlying system types) are runtime types
            return RuntimeTypeHandle.CanCastTo(fromType, this);
        }

希望这告诉您 Nullable 类型在框架中有特殊支持,无法复制。由于 Reflection 利用了这种支持,您将无法复制 Nullable<T>

的一些细微差别