Lựa chọn

 Danh mục

 Chi tiết tài nguyên
Fast Dynamic Property Access with C#
   Đăng bởi: host | Ngày : 01:43 05/12/09 | Xem: 598 |        
Fast Dynamic Property Access with C#
Introduction
Reflection is very useful for dynamic processing. However, if you have to repeatedly reflect on a property, say in a processing loop, you'll soon find that it can lead to performance problems. I ran into this particular problem in developing a rules engine that will be able to validate collections. I thought I would share this snippet of code, since I think it could be used in a variety of situations.
In this article I'll provide a fast, alternative solution for dynamic property access.
Implementation
My goal was to develop a class that could create a Type at runtime for direct access to the Get and Set methods of a property. This class would be provided with the target object Type and the property name it should access. I had considered runtime compiling, but then I learned about Reflection.Emit and its ability to create types at runtime through the use of MSIL. This was my first experience writing MSIL code and I found Ben Ratzlaff's Populating a PropertyGrid using Reflection.Emit to be a very helpful start.
In order to be able to compile code against against a Type that will be generated at runtime, an interface had to be created to define the generated types.
/// <summary>
/// The IPropertyAccessor interface defines a property
/// accessor.
/// </summary>
publicinterface 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);
}
The concrete PropertyAccessor class generates a Type at runtime that conforms to this interface and serves as a proxy layer to the generated Type. In its constructor it simply needs to be provided with the target object Type and the name of the property it should provide access to. All of the Reflection.Emit code is performed in the EmitAssembly method.
Collapse
/// <summary>
/// The PropertyAccessor class provides fast dynamic access
/// to a property of a specified target class.
/// </summary>
publicclass 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)
        {
            thrownew
              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();
            }
            returnthis.mEmittedPropertyAccessor.Get(target);
        }
        else
        {
            thrownew
              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>
    publicvoid Set(object target, object value)
    {
        if(mCanWrite)
        {
            if(this.mEmittedPropertyAccessor == null)
            {
                this.Init();
            }
            //
            // Set the property value
            //
            this.mEmittedPropertyAccessor.Set(target, value);
        }
        else
        {
            thrownew
              PropertyAccessorException(string.Format("Property \"{0}\" does" +
              " not have a set method.", mProperty));
        }
    }
 
    /// <summary>
    /// Whether or not the Property supports read access.
    /// </summary>
    publicbool CanRead
    {
        get
        {
            returnthis.mCanRead;
        }
    }
 
    /// <summary>
    /// Whether or not the Property supports write access.
    /// </summary>
    publicbool CanWrite
    {
        get
        {
            returnthis.mCanWrite;
        }
    }
 
    /// <summary>
    /// The Type of object this property accessor was
    /// created for.
    /// </summary>
    public Type TargetType
    {
        get
        {
            returnthis.mTargetType;
        }
    }
 
    /// <summary>
    /// The Type of the Property being accessed.
    /// </summary>
    public Type PropertyType
    {
        get
        {
            returnthis.mPropertyType;
        }
    }
 
    private Type mTargetType;
    private string mProperty;
    private Type mPropertyType;
    private IPropertyAccessor mEmittedPropertyAccessor;
    private Hashtable mTypeHash;
    privatebool mCanRead;
    privatebool mCanWrite;
 
    /// <summary>
    /// This method generates creates a new assembly containing
    /// the Type that will provide dynamic access.
    /// </summary>
    privatevoid 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)
        {
            thrownew Exception("Unable to create property accessor.");
        }
    }
 
    /// <summary>
    /// Thanks to Ben Ratzlaff for this snippet of code
    /// http://www.codeproject.com/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>
    privatevoid 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;
    }
}
Discussion
Although the PropertyAccessor class must reflect on the target Type the first time the property is accessed (for either read or write), this reflection only has to be done once. All subsequent calls to Get or Set will use the generated IL code.
Run the sample project for a performance demonstration. I've also included an NUnit test fixture.
 
Bình luận

Thêm nhận xét