Типы значений Reflection и Boxing

Я пишу класс, способный получать и устанавливать значения из объекта, используя шаблон строки, посредством отражения. Класс работает хорошо, даже на сложных шаблонах, но я получил неожиданное поведение, которое я не знаю, как решить/обойти.

По сути, когда класс обращается к полю или свойству, которое является типом значения, все работает, но он работает с копией типа значения. Действительно, когда я должен был установить значение с помощью строкового шаблона, реальный тип значения не обновлялся.

Класс поддерживает ссылку object и экземпляр MemberInfo (эти объекты получаются путем анализа шаблона доступа к корневому объекту); таким образом я могу получить или установить член, указанный MemberInfo, начиная с экземпляра object.

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));
    }
}

Когда параметр obj является структурным значением, возникает ошибка: я получаю/устанавливаю значение из коробочного значения.

Как я могу обойти это? Я уже проверил этот вопрос, но безуспешно (код можно посмотреть на управлении полями): бокс все равно происходит, так как я присваиваю значение поля объектной переменной.

Чтобы сделать вещи более понятными, вот полный код рассматриваемого класса:

// 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
    }
}

person Luca    schedule 29.07.2012    source источник
comment
Как насчет того, чтобы сделать эти два метода универсальными? Таким образом, вы избежите упаковки/распаковки.   -  person Nikola Anusev    schedule 29.07.2012
comment
Я не знаю тип объекта и работаю только с экземплярами объекта.   -  person Luca    schedule 29.07.2012
comment
Поможет ли передача вашего объекта в качестве параметра ref?   -  person Matt Mitchell    schedule 29.07.2012
comment
Является ли obj экземпляром изменяемой структуры?   -  person Jeppe Stig Nielsen    schedule 29.07.2012
comment
Многие считают изменяемые структуры вредными. Это одна из причин. Даже без отражения можно запутаться, когда мутируешь копию.   -  person Jeppe Stig Nielsen    schedule 29.07.2012
comment
@JeppeStigNielsen Меня не волнует, что многие разработчики думают об изменяемых структурах: очевидно, есть веская причина, по которой объект разработан как изменяемая структура.   -  person Luca    schedule 29.07.2012
comment
@Luca Объекты в С# не являются изменяемыми структурами. Семантически они очень разные. structs обрабатываются средой CLR очень особым образом. Например, структуры автоматически переопределяют оператор ==, и если их размер меньше 16 байт, они делают это способами, невозможными в системе типов С#.   -  person Michael Graczyk    schedule 30.07.2012
comment
@Luca Возможно, вы захотите работать над своим решением напрямую в IL. Кажется, что многое из того, что вы пытаетесь сделать, нелегко выразить на С#, но легко на IL.   -  person Michael Graczyk    schedule 30.07.2012


Ответы (2)


__makeref — недокументированное ключевое слово. Я никогда не видел, чтобы он использовался раньше, поэтому не знаю точно, что он делает. Однако вы можете выполнить то, что, как я предполагаю, пытается сделать __makeref, просто приведя тип значения к object перед изменением.

Джон Скит объясняет особенности в этом ответе

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

С другой стороны, недокументированные вещи могут меняться со временем. Я бы не стал полагаться на них для производственного кода.

person Eric J.    schedule 29.07.2012
comment
Вы пытались просто привести его к object, как показывает Джон в ответе, на который я ссылаюсь? - person Eric J.; 29.07.2012

Если вы объявите свой параметр obj как переменную ref, то, возможно, вы сможете вернуть его обратно после того, как изменили свою структуру. Это изменяемая/изменяемая структура?

Я не уверен, почему важно видеть, является ли тип поля типом значения. Я думал, мы обсуждаем случай, когда obj.GetType().IsValueType?

Дополнение:

Я немного подумал и больше не думаю, что получится сделать параметр ref если у вас бокс. Это даже не должно быть необходимо.

Я думаю, что ваша проблема только с методом Set? Похоже, вы не указали свое использование SetObjectMemberValue. Но я подозреваю, что вы хотите использовать его так:

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

Это никогда не может работать со структурой, потому что это упакованная копия, которую вы передаете методу. Независимо от того, что делает метод, он имеет доступ только к этой копии. Вместо этого вы могли бы сказать:

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

Если вам это не нравится, попробуйте сделать метод универсальным в типе obj. Тогда подпись может быть SetObjectMemberValue<TObj>(TObj obj, MemberInfo memberInfo, params object[] memberArgs). С универсальным типом упаковка не происходит, но вам, вероятно, придется использовать магию __makeref или сделать параметр ref (то есть ref TObj obj) с переназначением внутри тела метода. См. ветку переполнения стека, на которую вы ссылаетесь в своем вопросе.

person Jeppe Stig Nielsen    schedule 29.07.2012
comment
Я не понимаю, как параметр ref может работать в моем решении (см. рассматриваемый код). Не могли бы вы уточнить? - person Luca; 29.07.2012
comment
@Luca Я уточнил (см. Выше). - person Jeppe Stig Nielsen; 30.07.2012