简体   繁体   中英

Reflection and boxing value types

I'm writing a class able to get and set values from an object by using a string pattern, by means of reflection. The class works well, even on complex patterns, but I got un expected behaviour that I don't know how to solve/workaround.

Essentially, when the class is accessing to a field or property that is a value type, everything works, but it operates on a copy of the value type. Indeed, when I was to set a value using a string pattern, the real value type is not being updated.

The class mantains an object reference and a MemberInfo instance (those objects are got by analysing the access pattern on a root object); in this way I can get or set the member specified by MemberInfo starting from the object instance.

private static object GetObjectMemberValue(object obj, MemberInfo memberInfo, object[] memberArgs)
{
    if (memberInfo == null)
        throw new ArgumentNullException("memberInfo");

    // Get the value
    switch (memberInfo.MemberType) {
        case MemberTypes.Field: {
                FieldInfo fieldInfo = (FieldInfo)memberInfo;

                if (fieldInfo.FieldType.IsValueType) {
                    TypedReference typedReference = __makeref(obj);
                    return (fieldInfo.GetValueDirect(typedReference));
                } else
                    return (fieldInfo.GetValue(obj));
            }
        case MemberTypes.Property:
            return (((PropertyInfo)memberInfo).GetValue(obj, memberArgs));
        case MemberTypes.Method:
            return (((MethodInfo)memberInfo).Invoke(obj, memberArgs));
        default:
            throw new InvalidOperationException(String.Format("the type of the member {0}.{1} is not supported", obj.GetType().Name, memberInfo.Name));
    }
}

private static void SetObjectMemberValue(object obj, MemberInfo memberInfo, params object[] memberArgs)
{
    if (memberInfo == null)
        throw new ArgumentNullException("memberInfo");

    // Set the value
    switch (memberInfo.MemberType) {
        case MemberTypes.Field: {
                FieldInfo fieldInfo = (FieldInfo)memberInfo;

                if (fieldInfo.FieldType.IsValueType) {
                    TypedReference typedReference = __makeref(obj);
                    fieldInfo.SetValueDirect(typedReference, memberArgs[0]);
                } else
                    fieldInfo.SetValue(obj, memberArgs[0]);
            } break;
        case MemberTypes.Property:
            ((PropertyInfo)memberInfo).SetValue(obj, memberArgs[0], null);
            break;
        case MemberTypes.Method:
            ((MethodInfo)memberInfo).Invoke(obj, memberArgs);
            break;
        default:
            throw new InvalidOperationException(String.Format("the type of the member {0}.{1} is not supported", obj.GetType().Name, memberInfo.Name));
    }
}

When the obj parameter is a struct value, it happens the error: I get/set from the boxed value.

How can I workaround this? I've already checked this question , but without success (you can see the code on field management): the boxing happens all the same since I assign the field value into a object variable.

The make things more clear, here is the complete code of class in question:

// Copyright (C) 2012 Luca Piccioni
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//  
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//  
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

namespace Derm
{
    /// <summary>
    /// Class able to read and write a generic object.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This class supports the access to one of the following:
    /// - A specific object field
    /// - A specific object property (even indexed)
    /// - A specific object method (even with arguments)
    /// </para>
    /// </remarks>
    public class ObjectAccessor
    {
        #region Constructors

        /// <summary>
        /// Construct an ObjectAccessor that access to an object's field or property.
        /// </summary>
        /// <param name="container">
        /// A <see cref="System.Object"/> that specify a generic member.
        /// </param>
        /// <param name="memberPattern">
        /// A <see cref="System.String"/> that specify the pattern of the member of <paramref name="container"/>.
        /// </param>
        public ObjectAccessor(object container, string memberPattern)
        {
            if (container == null)
                throw new ArgumentNullException("container");
            if (memberPattern == null)
                throw new ArgumentNullException("memberPattern");

            // Store member pattern
            mMemberPattern = memberPattern;

            Dictionary<int, string> stringMap = new Dictionary<int,string>();
            object containerMember = container;
            int stringMapIndex = 0;

            // Remove (temporarly) strings enclosed by double-quotes
            memberPattern = Regex.Replace(memberPattern, "\"[^\\\"]*\"", delegate(Match match) {
                stringMap[stringMapIndex++] = match.Value;

                return (String.Format("{{{0}}}", stringMapIndex - 1));
            });

            string[] members = Regex.Split(memberPattern, @"\.");

            // Restore strings enclosed by double-quotes
            for (int i = 0; i < members.Length; i++ ) {
                members[i] = Regex.Replace(members[i], @"{(?<StringOrder>\d+)}", delegate(Match match) {
                    return (stringMap[Int32.Parse(match.Groups["StringOrder"].Value)]);
                });
            }

            if (members.Length > 1) {
                StringBuilder containerMemberPattern = new StringBuilder(memberPattern.Length);

                for (int i = 0; i < members.Length - 1; i++ ) {
                    MemberInfo memberInfo;
                    object[] memberArgs;

                    // Pattern for exception message
                    containerMemberPattern.AppendFormat(".{0}", members[i]);
                    // Access to the (intermediate) member
                    GetObjectMember(containerMember, members[i], out memberInfo, out memberArgs);
                    // Get member value
                    containerMember = GetObjectMemberValue(containerMember, memberInfo, memberArgs);
                    if (containerMember == null)
                        throw new InvalidOperationException(String.Format("the field {0} is null", containerMemberPattern.ToString()));
                    if ((memberInfo.MemberType != MemberTypes.Field) && (containerMember.GetType().IsValueType == true))
                        throw new NotSupportedException("invalid pattern becuase operating on strcuture copy");
                }
            }

            // Store container object
            mContainer = container;
            // Store object
            mObject = containerMember;
            // Get member
            GetObjectMember(mObject, members[members.Length - 1], out mMember, out mMemberArgs);
        }

        #endregion

        #region Object Access

        /// <summary>
        /// Get the type of the accessed member.
        /// </summary>
        public Type MemberType 
        {
            get
            {
                switch (mMember.MemberType) {
                    case MemberTypes.Field:
                        return (((FieldInfo)mMember).FieldType);
                    case MemberTypes.Property:
                        return (((PropertyInfo)mMember).PropertyType);
                    default:
                        throw new NotSupportedException(mMember.MemberType + " is not supported");
                }
            }
        }

        /// <summary>
        /// Get the value of the object member.
        /// </summary>
        /// <returns></returns>
        public object Get()
        {
            switch (mMember.MemberType) {
                case MemberTypes.Field: {
                        FieldInfo fieldInfo = (FieldInfo)mMember;

                        if (fieldInfo.FieldType.IsValueType) {
                            object referenceObject = mObject;
                            TypedReference typedReference = __makeref(referenceObject);
                            return (fieldInfo.GetValueDirect(typedReference));
                        } else
                            return (fieldInfo.GetValue(mObject));
                    }
                case MemberTypes.Property:
                    if (((PropertyInfo)mMember).CanRead == false)
                        throw new InvalidOperationException("write-only property");
                    return (((PropertyInfo)mMember).GetValue(mObject, null));
                default:
                    throw new NotSupportedException(mMember.MemberType + " is not supported");
            }
        }

        /// <summary>
        /// Set the value of the object member.
        /// </summary>
        /// <param name="value"></param>
        public void Set(object value)
        {
            switch (mMember.MemberType) {
                case MemberTypes.Field: {
                        FieldInfo fieldInfo = (FieldInfo)mMember;

                        if (fieldInfo.FieldType.IsValueType) {
                            object referenceObject = mObject;
                            TypedReference typedReference = __makeref(referenceObject);
                            fieldInfo.SetValueDirect(typedReference, value);
                        } else
                            fieldInfo.SetValue(mObject, value);
                    } break;
                case MemberTypes.Property:
                    if (((PropertyInfo)mMember).CanWrite == false)
                        throw new InvalidOperationException("read-only property");
                    ((PropertyInfo)mMember).SetValue(mObject, value, null);
                    break;
                default:
                    throw new NotSupportedException(mMember.MemberType + " is not supported");
            }
        }

        /// <summary>
        /// The object used for getting the object implementing <see cref="mMember"/>. In simple cases
        /// it equals <see cref="mObject"/>.
        /// </summary>
        private readonly object mContainer;

        /// <summary>
        /// The object that specify the field/property pointed by <see cref="mMember"/>.
        /// </summary>
        private readonly object mObject;

        /// <summary>
        /// The pattern used for getting/setting the member of <see cref="mObject"/>.
        /// </summary>
        private readonly string mMemberPattern;

        /// <summary>
        /// Field, property or method member of <see cref="mObject"/>.
        /// </summary>
        private readonly MemberInfo mMember;

        /// <summary>
        /// Arguments list specified at member invocation.
        /// </summary>
        private readonly object[] mMemberArgs;

        #endregion

        #region Object Member Access

        /// <summary>
        /// Access to an object member.
        /// </summary>
        /// <param name="obj">
        /// A <see cref="System.Object"/> which type defines the underlying member.
        /// </param>
        /// <param name="memberPattern">
        /// A <see cref="System.String"/> that specify how the member is identified. For methods and indexed properties, the arguments
        /// list is specified also.
        /// </param>
        /// <param name="memberInfo">
        /// A <see cref="System.Reflection.MemberInfo"/> that represent the member.
        /// </param>
        /// <param name="memberArgs">
        /// An array of <see cref="System.Object"/> that represent the argument list required for calling a method or an indexed
        /// property.
        /// </param>
        private static void GetObjectMember(object obj, string memberPattern, out MemberInfo memberInfo, out object[] memberArgs)
        {
            if (obj == null)
                throw new ArgumentNullException("obj");
            if (memberPattern == null)
                throw new ArgumentNullException("memberPattern");

            Type objType = obj.GetType();
            Match methodMatch;

            if ((methodMatch = sCollectionRegex.Match(memberPattern)).Success || (methodMatch = sMethodRegex.Match(memberPattern)).Success) {
                MemberInfo[] members = objType.GetMember(methodMatch.Groups["MethodName"].Value);
                ParameterInfo[] methodArgsInfo;
                int bestMemberIndex = 0;

                if ((members == null) || (members.Length == 0))
                    throw new InvalidOperationException(String.Format("no property/method {0}", memberPattern));

                string[] args = Regex.Split(methodMatch.Groups["MethodArgs"].Value, " *, *");

                if (members.Length != 1) {
                    Type[] argsType = new Type[args.Length];

                    bestMemberIndex = -1;

                    // Try to guess method arguments type to identify the best overloaded match
                    for (int i = 0; i < args.Length; i++)
                        argsType[i] = GuessMethodArgumentType(args[i]);

                    if (Array.TrueForAll<Type>(argsType, delegate(Type type) { return (type != null); })) {
                        for (int i = 0; i < members.Length; i++) {
                            if (members[i].MemberType == MemberTypes.Property) {
                                methodArgsInfo = ((PropertyInfo)members[i]).GetIndexParameters();
                                Debug.Assert((methodArgsInfo != null) && (methodArgsInfo.Length > 0));
                            } else if (members[i].MemberType == MemberTypes.Method) {
                                methodArgsInfo = ((MethodInfo)members[i]).GetParameters();
                            } else
                                throw new NotSupportedException("neither a method or property");

                            // Parameters count mismatch?
                            if (methodArgsInfo.Length != args.Length)
                                continue;
                            // Parameter type incompatibility?
                            bool compatibleArgs = true;

                            for (int j = 0; j < args.Length; j++) {
                                if (argsType[j] != methodArgsInfo[j].ParameterType) {
                                    compatibleArgs = false;
                                    break;
                                }
                            }

                            if (compatibleArgs == false)
                                continue;

                            bestMemberIndex = i;
                            break;
                        }
                    }

                    if (bestMemberIndex == -1)
                        throw new InvalidOperationException(String.Format("method or property {0} has an ambiguous definition", memberPattern));
                }

                // Method or indexed property
                memberInfo = members[bestMemberIndex];
                // Parse method arguments
                if (memberInfo.MemberType == MemberTypes.Property) {
                    methodArgsInfo = ((PropertyInfo)memberInfo).GetIndexParameters();
                    Debug.Assert((methodArgsInfo != null) && (methodArgsInfo.Length > 0));
                } else if (memberInfo.MemberType == MemberTypes.Method) {
                    methodArgsInfo = ((MethodInfo)memberInfo).GetParameters();
                } else
                    throw new NotSupportedException("neither a method or property");

                if (args.Length != methodArgsInfo.Length)
                    throw new InvalidOperationException("argument count mismatch");

                memberArgs = new object[args.Length];
                for (int i = 0; i < args.Length; i++) {
                    Type argType = methodArgsInfo[i].ParameterType;

                    if (argType == typeof(String)) {
                        memberArgs[i] = args[i].Substring(1, args[i].Length - 2);
                    } else if (argType == typeof(Int32)) {
                        memberArgs[i] = Int32.Parse(args[i]);
                    } else if (argType == typeof(UInt32)) {
                        memberArgs[i] = UInt32.Parse(args[i]);
                    } else if (argType == typeof(Single)) {
                        memberArgs[i] = Single.Parse(args[i]);
                    } else if (argType == typeof(Double)) {
                        memberArgs[i] = Double.Parse(args[i]);
                    } else if (argType == typeof(Int16)) {
                        memberArgs[i] = Int16.Parse(args[i]);
                    } else if (argType == typeof(UInt16)) {
                        memberArgs[i] = UInt16.Parse(args[i]);
                    } else if (argType == typeof(Char)) {
                        memberArgs[i] = Char.Parse(args[i]);
                    } else if (argType == typeof(Byte)) {
                        memberArgs[i] = Byte.Parse(args[i]);
                    } else
                        throw new InvalidOperationException(String.Format("argument of type {0} is not supported", argType.Name));
                }
            } else {
                MemberInfo[] members = objType.GetMember(memberPattern);

                if ((members == null) || (members.Length == 0))
                    throw new InvalidOperationException(String.Format("no property/field {0}", memberPattern));

                if (members.Length > 1) {
                    members = Array.FindAll<MemberInfo>(members, delegate(MemberInfo member) {
                        return (member.MemberType == MemberTypes.Property || member.MemberType == MemberTypes.Field);
                    });
                }

                if (members.Length != 1)
                    throw new InvalidOperationException(String.Format("field of property {0} has an ambiguous definition", memberPattern));

                // Property of field
                memberInfo = members[0];
                // Not an indexed property
                memberArgs = null;
            }
        }

        /// <summary>
        /// Access to the object member.
        /// </summary>
        /// <param name="obj">
        /// A <see cref="System.Object"/> which type defines the underlying member.
        /// </param>
        /// <param name="memberInfo">
        /// A <see cref="System.Reflection.MemberInfo"/> that represent the member.
        /// </param>
        /// <param name="memberArgs">
        /// An array of <see cref="System.Object"/> that represent the argument list required for calling a method or an indexed
        /// property.
        /// </param>
        /// <returns></returns>
        private static object GetObjectMemberValue(object obj, MemberInfo memberInfo, object[] memberArgs)
        {
            if (memberInfo == null)
                throw new ArgumentNullException("memberInfo");

            // Get the value
            switch (memberInfo.MemberType) {
                case MemberTypes.Field: {
                        FieldInfo fieldInfo = (FieldInfo)memberInfo;

                        if (fieldInfo.FieldType.IsValueType) {
                            TypedReference typedReference = __makeref(obj);
                            return (fieldInfo.GetValueDirect(typedReference));
                        } else
                            return (fieldInfo.GetValue(obj));
                    }
                case MemberTypes.Property:
                    return (((PropertyInfo)memberInfo).GetValue(obj, memberArgs));
                case MemberTypes.Method:
                    return (((MethodInfo)memberInfo).Invoke(obj, memberArgs));
                default:
                    throw new InvalidOperationException(String.Format("the type of the member {0}.{1} is not supported", obj.GetType().Name, memberInfo.Name));
            }
        }

        private static void SetObjectMemberValue(object obj, MemberInfo memberInfo, params object[] memberArgs)
        {
            if (memberInfo == null)
                throw new ArgumentNullException("memberInfo");

            // Set the value
            switch (memberInfo.MemberType) {
                case MemberTypes.Field: {
                        FieldInfo fieldInfo = (FieldInfo)memberInfo;

                        if (fieldInfo.FieldType.IsValueType) {
                            TypedReference typedReference = __makeref(obj);
                            fieldInfo.SetValueDirect(typedReference, memberArgs[0]);
                        } else
                            fieldInfo.SetValue(obj, memberArgs[0]);
                    } break;
                case MemberTypes.Property:
                    ((PropertyInfo)memberInfo).SetValue(obj, memberArgs[0], null);
                    break;
                case MemberTypes.Method:
                    ((MethodInfo)memberInfo).Invoke(obj, memberArgs);
                    break;
                default:
                    throw new InvalidOperationException(String.Format("the type of the member {0}.{1} is not supported", obj.GetType().Name, memberInfo.Name));
            }
        }


        private static Type GuessMethodArgumentType(string methodArg)
        {
            if (String.IsNullOrEmpty(methodArg))
                throw new ArgumentNullException("methodArg");

            if (sMethodArgString.IsMatch(methodArg))
                return (typeof(String));



            return (null);
        }

        /// <summary>
        /// Regular expression used for matching method calls.
        /// </summary>
        private static readonly Regex sMethodRegex = new Regex(@"^(?<MethodName>\w+) *\( *(?<MethodArgs>.*) *\)$");

        /// <summary>
        /// Regular expression used for matching method string arguments.
        /// </summary>
        private static readonly Regex sMethodArgString = new Regex(@"\"".*\""");

        /// <summary>
        /// Regular expression used for matching collection indexer calls.
        /// </summary>
        private static readonly Regex sCollectionRegex = new Regex(@"^(?<MethodName>\w+) *\[ *(?<MethodArgs>.*) *\]$");

        #endregion
    }
}

__makeref is an undocumented keyword. I have never seen it used before so don't know exactly what it is doing. However, you can accomplish what I assume __makeref is trying to do just by casting the value type to object before modifying.

Jon Skeet explains the specifics in this answer

https://stackoverflow.com/a/6280540/141172

On a side note, undocumented things have a way of changing over time. I would not rely on them for production code.

If you declare your obj parameter as a ref variable, then maybe you can assign back to it after you changed your struct. Is this a mutable/changable struct?

I'm not sure why it's relevant to see if the field type is a value type. I thought we were discussing the case where obj.GetType().IsValueType ?

Addition:

I've thought a bit about it, and I no longer think it will work to make the parameter ref if you have boxing . It shouldn't even be necessary.

I think your problem is only with the Set method? It looks like you didn't include your use of SetObjectMemberValue . But I suspect you want to use it like this:

var myMutableStruct = XXX;
SetObjectMemberValue(myMutableStruct, instanceFieldInfo, 42);
// use myMutableStruct with new field value

This can never work with a struct, because it's a boxed copy you pass to the method. No matter what the method does, it has only access to that copy. Instead you could say:

var myMutableStruct = XXX;
object boxToKeep = myMutableStruct;
SetObjectMemberValue(myMutableStruct, instanceFieldInfo, 42);
myMutableStruct = (MyMutableStruct)boxToKeep;
// use myMutableStruct with new field value

If you don't like this, try making the method generic in the type of obj . The signature could then be SetObjectMemberValue<TObj>(TObj obj, MemberInfo memberInfo, params object[] memberArgs) . With a generic type, no boxing occurs, but you will probably need to use the magic of __makeref or make the parameter ref (so ref TObj obj ) with reassignment inside the method body. See the Stack Overflow thread you link yourself in your question.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM