Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / All-Topics

FieldInfo / PropertyInfo -- GetValue / SetValue Alternatives

5.00/5 (3 votes)
21 May 2015CPOL1 min read 13.5K  
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)