65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的快速动态属性访问

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (48投票s)

2005年3月23日

CPOL

2分钟阅读

viewsIcon

318841

downloadIcon

2269

使用反射访问属性很方便,但通常速度太慢。本文描述了一种用于动态属性访问的替代方法。

Sample Image - Fast_Dynamic_Properties.png

引言

反射对于动态处理非常有用。但是,如果您需要在处理循环中重复反射一个属性,很快就会发现它会导致性能问题。我在开发一个能够验证集合的规则引擎时遇到了这个问题。我想分享这段代码,因为我认为它可以在各种情况下使用。

在本文中,我将提供一种快速的动态属性访问替代方案。

实现

我的目标是开发一个类,该类可以在运行时创建一个 Type,以便直接访问属性的 GetSet 方法。该类将提供目标对象 Type 和它应该访问的属性名称。我曾考虑过运行时编译,但后来我了解了 Reflection.Emit 及其通过使用 MSIL 在运行时创建类型的能力。这是我第一次编写 MSIL 代码,我发现 Ben Ratzlaff 的 使用 Reflection.Emit 填充 PropertyGrid 对我来说是一个非常有帮助的起点。

为了能够针对将在运行时生成的 Type 编译代码,必须创建一个接口来定义生成的类型。

/// <summary>
/// The IPropertyAccessor interface defines a property
/// accessor.
/// </summary>
public interface IPropertyAccessor
{
    /// <summary>
    /// Gets the value stored in the property for
    /// the specified target.
    /// </summary>
    /// <param name="target">Object to retrieve
    /// the property from.</param>
    /// <returns>Property value.</returns>
    object Get(object target);
    /// <summary>
    /// Sets the value for the property of
    /// the specified target.
    /// </summary>
    /// <param name="target">Object to set the
    /// property on.</param>
    /// <param name="value">Property value.</param>
    void Set(object target, object value);
}

具体的 PropertyAccessor 类在运行时生成一个符合此接口的 Type,并作为生成 Type 的代理层。在它的构造函数中,只需要提供目标对象 Type 和它应该提供访问权限的属性名称。所有的 Reflection.Emit 代码都在 EmitAssembly 方法中执行。

/// <summary>
/// The PropertyAccessor class provides fast dynamic access
/// to a property of a specified target class.
/// </summary>
public class PropertyAccessor : IPropertyAccessor
{
    /// <summary>
    /// Creates a new property accessor.
    /// </summary>
    /// <param name="targetType">Target object type.</param>
    /// <param name="property">Property name.</param>
    public PropertyAccessor(Type targetType, string property)
    {
        this.mTargetType = targetType;
        this.mProperty = property;
        PropertyInfo propertyInfo = 
            targetType.GetProperty(property);
        //
        // Make sure the property exists
        //
        if(propertyInfo == null)
        {
            throw new 
              PropertyAccessorException(string.Format("Property \"{0}\" does" + 
              " not exist for type " + "{1}.", property, targetType));
        }
        else
        {
            this.mCanRead = propertyInfo.CanRead;
            this.mCanWrite = propertyInfo.CanWrite;
            this.mPropertyType = propertyInfo.PropertyType;
        }
    }

    /// <summary>
    /// Gets the property value from the specified target.
    /// </summary>
    /// <param name="target">Target object.</param>
    /// <returns>Property value.</returns>
    public object Get(object target)
    {
        if(mCanRead)
        {
            if(this.mEmittedPropertyAccessor == null)
            {
                this.Init();
            }
            return this.mEmittedPropertyAccessor.Get(target);
        }
        else
        {
            throw new 
              PropertyAccessorException(string.Format("Property \"{0}\" does" + 
              " not have a get method.", mProperty));
        }
    }

    /// <summary>
    /// Sets the property for the specified target.
    /// </summary>
    /// <param name="target">Target object.</param>
    /// <param name="value">Value to set.</param>
    public void Set(object target, object value)
    {
        if(mCanWrite)
        {
            if(this.mEmittedPropertyAccessor == null)
            {
                this.Init();
            }
            //
            // Set the property value
            //
            this.mEmittedPropertyAccessor.Set(target, value);
        }
        else
        {
            throw new 
              PropertyAccessorException(string.Format("Property \"{0}\" does" + 
              " not have a set method.", mProperty));
        }
    }

    /// <summary>
    /// Whether or not the Property supports read access.
    /// </summary>
    public bool CanRead
    {
        get
        {
            return this.mCanRead;
        }
    }

    /// <summary>
    /// Whether or not the Property supports write access.
    /// </summary>
    public bool CanWrite
    {
        get
        {
            return this.mCanWrite;
        }
    }

    /// <summary>
    /// The Type of object this property accessor was
    /// created for.
    /// </summary>
    public Type TargetType
    {
        get
        {
            return this.mTargetType;
        }
    }

    /// <summary>
    /// The Type of the Property being accessed.
    /// </summary>
    public Type PropertyType
    {
        get
        {
            return this.mPropertyType;
        }
    }

    private Type mTargetType;
    private string mProperty;
    private Type mPropertyType;
    private IPropertyAccessor mEmittedPropertyAccessor;
    private Hashtable mTypeHash;
    private bool mCanRead;
    private bool mCanWrite;

    /// <summary>
    /// This method generates creates a new assembly containing
    /// the Type that will provide dynamic access.
    /// </summary>
    private void Init()
    {
        this.InitTypes();
        // Create the assembly and an instance of the 
        // property accessor class.
        Assembly assembly = EmitAssembly();
        mEmittedPropertyAccessor = 
          assembly.CreateInstance("Property") as IPropertyAccessor;
        if(mEmittedPropertyAccessor == null)
        {
            throw new Exception("Unable to create property accessor.");
        }
    }

    /// <summary>
    /// Thanks to Ben Ratzlaff for this snippet of code
    /// https://codeproject.org.cn/cs/miscctrl/CustomPropGrid.asp
    /// 
    /// "Initialize a private hashtable with type-opCode pairs 
    /// so i dont have to write a long if/else statement when outputting msil"
    /// </summary>
    private void InitTypes()
    {
        mTypeHash=new Hashtable();
        mTypeHash[typeof(sbyte)]=OpCodes.Ldind_I1;
        mTypeHash[typeof(byte)]=OpCodes.Ldind_U1;
        mTypeHash[typeof(char)]=OpCodes.Ldind_U2;
        mTypeHash[typeof(short)]=OpCodes.Ldind_I2;
        mTypeHash[typeof(ushort)]=OpCodes.Ldind_U2;
        mTypeHash[typeof(int)]=OpCodes.Ldind_I4;
        mTypeHash[typeof(uint)]=OpCodes.Ldind_U4;
        mTypeHash[typeof(long)]=OpCodes.Ldind_I8;
        mTypeHash[typeof(ulong)]=OpCodes.Ldind_I8;
        mTypeHash[typeof(bool)]=OpCodes.Ldind_I1;
        mTypeHash[typeof(double)]=OpCodes.Ldind_R8;
        mTypeHash[typeof(float)]=OpCodes.Ldind_R4;
    }

    /// <summary>
    /// Create an assembly that will provide the get and set methods.
    /// </summary>
    private Assembly EmitAssembly()
    {
        //
        // Create an assembly name
        //
        AssemblyName assemblyName = new AssemblyName();
        assemblyName.Name = "PropertyAccessorAssembly";
        //
        // Create a new assembly with one module
        //
        AssemblyBuilder newAssembly = 
           Thread.GetDomain().DefineDynamicAssembly(assemblyName, 
           AssemblyBuilderAccess.Run);
        ModuleBuilder newModule = 
           newAssembly.DefineDynamicModule("Module");
        //
        // Define a public class named "Property" in the assembly.
        //
        TypeBuilder myType = 
           newModule.DefineType("Property", TypeAttributes.Public);
        //
        // Mark the class as implementing IPropertyAccessor. 
        //
        myType.AddInterfaceImplementation(typeof(IPropertyAccessor));
        // Add a constructor
        ConstructorBuilder constructor = 
           myType.DefineDefaultConstructor(MethodAttributes.Public);
        //
        // Define a method for the get operation. 
        //
        Type[] getParamTypes = new Type[] {typeof(object)};
        Type getReturnType = typeof(object);
        MethodBuilder getMethod = 
          myType.DefineMethod("Get", 
          MethodAttributes.Public | MethodAttributes.Virtual, 
          getReturnType, 
          getParamTypes);
        //
        // From the method, get an ILGenerator. This is used to
        // emit the IL that we want.
        //
        ILGenerator getIL = getMethod.GetILGenerator();

        //
        // Emit the IL. 
        //
        MethodInfo targetGetMethod = this.mTargetType.GetMethod("get_" + 
                                                    this.mProperty);
        if(targetGetMethod != null)
        {
            getIL.DeclareLocal(typeof(object));
            getIL.Emit(OpCodes.Ldarg_1); //Load the first argument
            //(target object)
            //Cast to the source type
            getIL.Emit(OpCodes.Castclass, this.mTargetType);
            //Get the property value
            getIL.EmitCall(OpCodes.Call, targetGetMethod, null);
            if(targetGetMethod.ReturnType.IsValueType)
            {
                getIL.Emit(OpCodes.Box, targetGetMethod.ReturnType);
                //Box if necessary
            }
            getIL.Emit(OpCodes.Stloc_0); //Store it

            getIL.Emit(OpCodes.Ldloc_0);
        }
        else
        {
            getIL.ThrowException(typeof(MissingMethodException));
        }
        getIL.Emit(OpCodes.Ret);

        //
        // Define a method for the set operation.
        //
        Type[] setParamTypes = new Type[] {typeof(object), typeof(object)};
        Type setReturnType = null;
        MethodBuilder setMethod = 
            myType.DefineMethod("Set", 
           MethodAttributes.Public | MethodAttributes.Virtual, 
           setReturnType, 
           setParamTypes);
        //
        // From the method, get an ILGenerator. This is used to
        // emit the IL that we want.
        //
        ILGenerator setIL = setMethod.GetILGenerator();
        //
        // Emit the IL. 
        //
        MethodInfo targetSetMethod = 
            this.mTargetType.GetMethod("set_" + this.mProperty);
        if(targetSetMethod != null)
        {
            Type paramType = targetSetMethod.GetParameters()[0].ParameterType;
            setIL.DeclareLocal(paramType);
            setIL.Emit(OpCodes.Ldarg_1); //Load the first argument 
            //(target object)
            //Cast to the source type
            setIL.Emit(OpCodes.Castclass, this.mTargetType);            
            setIL.Emit(OpCodes.Ldarg_2); //Load the second argument 
            //(value object)
            if(paramType.IsValueType)
            {
                setIL.Emit(OpCodes.Unbox, paramType); //Unbox it 
                if(mTypeHash[paramType]!=null) //and load
                {
                    OpCode load = (OpCode)mTypeHash[paramType];
                    setIL.Emit(load);
                }
                else
                {
                    setIL.Emit(OpCodes.Ldobj,paramType);
                }
            }
            else
            {
                setIL.Emit(OpCodes.Castclass, paramType); //Cast class
            }

            setIL.EmitCall(OpCodes.Callvirt, 
               targetSetMethod, null); //Set the property value
        }
        else
        {
            setIL.ThrowException(typeof(MissingMethodException));
        }
        setIL.Emit(OpCodes.Ret);
        //
        // Load the type
        //
        myType.CreateType();
        return newAssembly;
    }
}

讨论

虽然 PropertyAccessor 类必须在第一次访问属性时(无论是读取还是写入)反射目标 Type(以便进行读取或写入),但这种反射只需要执行一次。后续所有对 GetSet 的调用都将使用生成的 IL 代码。

运行示例项目以进行性能演示。我还包含了一个 NUnit 测试夹具。

© . All rights reserved.