Click here to Skip to main content
15,881,204 members
Articles / All Topics
Technical Blog

FieldInfo / PropertyInfo -- GetValue / SetValue Alternatives

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
21 May 2015CPOL1 min read 12.7K   5  
FieldInfo / PropertyInfo -- GetValue / SetValue Alternatives

I was writing a framework that required access to an application-specific set of fields/properties. The straight-forward approach is to use the GetValue/SetValue methods on FieldInfo and PropertyInfo. But everyone knows Reflection isn't the most performance savvy way to go about things. So I decided to write something that was faster.

First, I wanted to use Expressions. But I quickly realized that there were some real enhancements done for Expressions in 4.0, and I was limited to 3.5 (I'm doing work that needs to be included in Unity3d / Mono projects). The major issue was the lack of the Expression.Assign method found so nicely in 4.0, but sadly missing in 3.5.

Using the help of a surrogate assignment function, I worked around this limitation to come up with the following class. By constructing an object of this Type with a MemberInfo object (either FieldInfo or PropertyInfo), the object takes the place of the SetValue and GetValue from Reflection. The overall improvement of this class to Reflection was thirty-seven (37) times faster execution times. That's significant.

Of note- I could have had the application send my framework two delegates / lambdas that performed the get/set operations. This would have been better for performance, but worse for maintainability (two things instead of one that can get crossed-up) and worse for flexibility (what if the field names were in data somewhere and unknown at compile time).

The application must pass in a MemberInfo object from Reflection when it constructs an object of this Type.

C#
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;

namespace Oberberger.Morpheus
{
    /// <summary>
    /// For a given class and an expression identifying a property on that class, where the
    /// property is of any type, this class will allow rapid access to that property both
    /// for retrieval (get) and assignment (set).
    /// </summary>
    /// <remarks>
    /// Running tests show the following performance indications for Release-mode builds:
    /// 
    /// <code>
    /// Using a "raw" lambda for get/set 
    ///     var lambdaSetter = new Action{MyType, object}( ( _p, _v ) => _p.Something = (int) _v );
    ///     var lambdaGetter = new Func{MyType, object}( _p => _p.Something );
    ///     
    ///     lambdaSetter( obj, lambdaGetter( obj ) );
    /// 
    /// 
    /// Using this class
    ///     var access = new CGetterSetter(typeof(MyObj)).GetField("MyField");
    ///     
    ///     var val = access.Get( obj );
    ///     access.Set( obj, newVal );
    /// 
    /// 
    /// Using Reflection/PropertyInfo
    ///     var pi = GetType(MyType).GetProperty("Something");
    ///     
    ///     pi.SetValue( obj, pi.GetValue( obj, null ), null );
    ///     
    /// </code>
    /// 
    /// <code>
    /// Results based on the "raw lambda" being the baseline:
    /// 
    /// Using this CGetterSetter is ~2.91 times slower
    /// Using Reflection is ~108 times slower than app-provided lambda
    /// Using Reflection is ~37 times slower than CGetterSetter
    /// 
    /// </code>
    /// 
    /// The EXTREME performance increase over Reflection makes this class a very viable
    /// alternative. Further, other tests showed that there is ZERO performance hit when
    /// using the GET functionality- The SET functionality is where performance is hit.
    /// </remarks>

    /*Performance over 1 Billion runs (100 Million for Reflection). Numbers in Nanoseconds.
 
       /--------------------------------------------------+-------------------------\
       |                                                  |                         |
       V                                                  ^                         ^

     Lambda   RefMultiple             CGS    Multiple      Reflection   Multiple
     2,908  <---    1.00  Ref Field Get   4,373     1.50        107,830    37.08 
     5,160      ^   1.77  Ref Field Set  10,390     2.01        162,050    31.41 
     2,930      |   1.01  Val Field Get  11,198     3.82        145,780    49.75 
     3,363      |   1.16  Val Field Set  13,325     3.96        197,900    58.85 
     2,916      |   1.00  Ref Prop Get   6,324     2.17        495,730   170.00 
     5,214      |   1.79  Ref Prop Set   9,585     1.84        663,660   127.28 
     2,922      |   1.00  Val Prop Get  13,410     4.59        539,100   184.50 
     3,092      |   1.06  Val Prop Set  14,483     4.68        758,740   245.39 

Summaries
     11,676 <---    1.00  Getters          35,305  3.02      1,288,440   110.35 
     16,829  ^   1.44  Setters          47,783  2.84      1,782,350   105.91 
     16,198  |   1.39  Reference Types  30,672  1.89      1,429,270    88.24 
     12,307  |   1.05  Value Types      52,416  4.26      1,641,520   133.38 
     14,361  |   1.23  Fields          39,286  2.74        613,560    42.72 
     14,144  |   1.21  Properties      43,802  3.10      2,457,230   173.73 

     28,505           All              83,088     2.91        3,070,790    107.73 

    */

    /// <summary>
    /// A Property or Field accessor object that uses compiled Expressions to access a
    /// property or a field. Properties are assumed to have both getter and setter methods
    /// for the respective methods to be available. The access modifiers do not matter, as
    /// the application must already have a <see cref="PropertyInfo"/> or a
    /// <see cref="FieldInfo"/> object to use this class.
    /// </summary>
    public class CGetterSetter
    {
        /// <summary>
        /// Get the helper method signature one time
        /// </summary>
        private static MethodInfo sm_valueAssignerMethod = 
            typeof( CGetterSetter )
            .GetMethod( "ValueAssigner", BindingFlags.Static | BindingFlags.NonPublic );

        /// <summary>
        /// This is the internal method responsible for assigning one value to a member.
        /// This is required to make this class compliant with .NET 3.5 (Unity3d compatible)
        /// </summary>
        /// <typeparam name="T">The Type of the values to assign</typeparam>
        /// <param name="dest">The destination member</param>
        /// <param name="src">The value to assign</param>
        private static void ValueAssigner<T>( out T dest, T src )
        {
            dest = src;
        }

        /// <summary>
        /// The delegate for getting the value of the member
        /// </summary>
        private Func<object, object> m_getter;

        /// <summary>
        /// The delegate for setting the value of the member
        /// </summary>
        private Action<object, object> m_setter;

        /// <summary>
        /// Get the value of the member on a provided object.
        /// </summary>
        /// <param name="p_obj">The object to query for the member value</param>
        /// <returns>The value of the member on the provided object</returns>
        public object Get( object p_obj )
        {
            return m_getter( p_obj );
        }

        /// <summary>
        /// Set the value on a given object to a given value.
        /// </summary>
        /// <param name="p_obj">The object whose member value to set</param>
        /// <param name="p_value">The value to assign to the member</param>
        public void Set( object p_obj, object p_value )
        {
            m_setter( p_obj, p_value );
        }

        /// <summary>
        /// Construct a new member accessor based on a Reflection MemberInfo- either a
        /// PropertyInfo or a FieldInfo
        /// </summary>
        /// <param name="p_member">
        /// A PropertyInfo or a FieldInfo describing the member to access
        /// </param>
        public CGetterSetter( MemberInfo p_member )
        {
            if (p_member == null)
                throw new ArgumentNullException( "Must initialize with a non-null Field or Property" );

            MemberExpression exMember = null;

            if (p_member is FieldInfo)
            {
                var fi = p_member as FieldInfo;
                var assignmentMethod = sm_valueAssignerMethod.MakeGenericMethod( fi.FieldType );

                Init( fi.DeclaringType, fi.FieldType,
                    _ex => exMember = Expression.Field( _ex, fi ), // Create a Field expression, 
                    			// and SAVE that field expression for the Call expression
                    ( _, _val ) => Expression.Call( assignmentMethod, 
                    exMember, _val ) // We're going to call the static 
                                     // "ValueAssigner" method on this class
                );
            }
            else if (p_member is PropertyInfo)
            {
                var pi = p_member as PropertyInfo;
                var assignmentMethod = pi.GetSetMethod();

                Init( pi.DeclaringType, pi.PropertyType,
                    _ex => exMember = Expression.Property( _ex, pi ), // Create a Property expression
                    ( _obj, _val ) => Expression.Call
                    ( _obj, assignmentMethod, _val ) // We're going to call 
                                                     // the SetMethod on the PropertyInfo object
                );
            }
            else
            {
                throw new ArgumentException
                ( "The member must be either a Field or a Property, not " + p_member.MemberType );
            }
        }

        /// <summary>
        /// Internal initialization routine. The difference between Field and Property
        /// access is extremely similar, but just different enough to require the two
        /// delegates back into the calling routine provide the specialized information.
        /// </summary>
        /// <param name="p_objectType">
        /// The Type of the objects that will have this member accessed
        /// </param>
        /// <param name="p_valueType">The Type of the member</param>
        /// <param name="p_fnGetMember">
        /// A delegate that returns the correct Expression for the member- either
        /// <see cref="Expression.Property"/> or <see cref="Expression.Field"/>
        /// </param>
        /// <param name="p_fnMakeCallExpression">
        /// Get a method that actually calls the Assignment function appropriate for the
        /// MemberType. The order of the parameters for Fields vs Properties is slightly
        /// different, as the Field assignment is static while the Property assignment is an
        /// instance method.
        /// </param>
        private void Init(
            Type p_objectType,
            Type p_valueType,
            Func<Expression, MemberExpression> p_fnGetMember,
            Func<Expression, Expression, MethodCallExpression> p_fnMakeCallExpression )
        {
            var exObjParam = Expression.Parameter( typeof( object ), "theObject" );
            var exValParam = Expression.Parameter( typeof( object ), "theProperty" );

            var exObjConverted = Expression.Convert( exObjParam, p_objectType );
            var exValConverted = Expression.Convert( exValParam, p_valueType );

            Expression exMember = p_fnGetMember( exObjConverted );

            Expression getterMember = p_valueType.IsValueType ? 
                                 Expression.Convert( exMember, typeof( object ) ) : exMember;
            m_getter = Expression.Lambda<Func<object, object>>( getterMember, exObjParam ).Compile();

            Expression exAssignment = p_fnMakeCallExpression( exObjConverted, exValConverted );
            m_setter = Expression.Lambda<Action<object, object>>
                                   ( exAssignment, exObjParam, exValParam ).Compile();
        }

#if false // The following code was refactored because of the extreme similarities between the methods.
        public CGenGetterSetter( MemberInfo p_member )
        {
            if (p_member == null)
                throw new ArgumentNullException( "Must initialize with a non-null Field or Property" );

            if (p_member is FieldInfo)
                InitAsField( p_member as FieldInfo );
            else if (p_member is PropertyInfo)
                InitAsProperty( p_member as PropertyInfo );
            else
                throw new ArgumentException
                ( "The member must be either a Field or a Property, not " + p_member.MemberType );
        }

        private void InitAsProperty( PropertyInfo p_propertyInfo )
        {
            var objType = p_propertyInfo.DeclaringType;
            var valType = p_propertyInfo.PropertyType;

            var assignmentMethod = p_propertyInfo.GetSetMethod();



            var exObjParam = Expression.Parameter( typeof( object ), "theObject" );
            var exValParam = Expression.Parameter( typeof( object ), "theProperty" );

            var exObjConverted = Expression.Convert( exObjParam, objType );
            var exValConverted = Expression.Convert( exValParam, valType );

            /**/
            Expression exMember = Expression.Property( exObjConverted, p_propertyInfo );

            Expression getterMember = valType.IsValueType ? 
                       Expression.Convert( exMember, typeof( object ) ) : exMember;
            m_getter = Expression.Lambda<Func<object, object>>( getterMember, exObjParam ).Compile();

            /**/
            Expression exAssignment = Expression.Call
                             ( exObjConverted, assignmentMethod, exValConverted );
            m_setter = Expression.Lambda<Action<object, object>>
                       ( exAssignment, exObjParam, exValParam ).Compile();
        }

        private void InitAsField( FieldInfo p_fieldInfo )
        {
            var objType = p_fieldInfo.DeclaringType;
            var valType = p_fieldInfo.FieldType;

            var assignmentMethod = typeof( CGenGetterSetter )
                            .GetMethod( "ValueAssigner", BindingFlags.Static | BindingFlags.NonPublic )
                            .MakeGenericMethod( valType );

            var exObjParam = Expression.Parameter( typeof( object ), "theObject" );
            var exValParam = Expression.Parameter( typeof( object ), "theProperty" );

            var exObjConverted = Expression.Convert( exObjParam, objType );
            var exValConverted = Expression.Convert( exValParam, valType );

            /**/
            Expression exMember = Expression.Field( exObjConverted, p_fieldInfo );

            Expression getterMember = valType.IsValueType ? 
                Expression.Convert( exMember, typeof( object ) ) : exMember;
            m_getter = Expression.Lambda<Func<object, object>>( getterMember, exObjParam ).Compile();

            /**/
            var exAssignment = Expression.Call( assignmentMethod, exMember, exValConverted );
            m_setter = Expression.Lambda<Action<object, object>>
                      ( exAssignment, exObjParam, exValParam ).Compile();
        }
#endif

    }
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --