Как я могу получить поля, используемые в методе (.NET)?

В .NET, используя отражение, как я могу получить переменные класса, которые используются в методе?

Ex:

class A
{
    UltraClass B = new(..);
    SupaClass C = new(..);

    void M1()
    {
        B.xyz(); // it can be a method call
        int a = C.a; // a variable access
    }
}

Примечание. GetClassVariablesInMethod(M1 MethodInfo) возвращает переменные B и C. Под переменными я подразумеваю значение и/или тип и параметры конструктора этой конкретной переменной.


person kerem    schedule 16.09.2009    source источник
comment
Я не понимаю, что вы пытаетесь сделать. Зачем нужно отражение? Под переменными класса вы имеете в виду поля? Вы можете легко получить текущий экземпляр определенного поля, но не аргументы конструктора, которые используются для его создания. зачем вам это?   -  person Stefan Steinegger    schedule 17.09.2009
comment
Под переменными класса я подразумеваю поля класса, которые являются классами. Я думаю об объявлении атрибута для некоторых методов, которые требуют специальных действий в соответствии с переменными, которые он использует из своего родительского класса. Текущий экземпляр определенного поля может работать для меня.   -  person kerem    schedule 17.09.2009


Ответы (5)


Есть много разных ответов, но так как ни один мне не нравится, вот мой. Он использует мой читатель IL на основе отражения.

Вот метод, извлекающий все поля, используемые методом:

static IEnumerable<FieldInfo> GetUsedFields (MethodInfo method)
{
    return (from instruction in method.GetInstructions ()
           where instruction.OpCode.OperandType == OperandType.InlineField
           select (FieldInfo) instruction.Operand).Distinct ();
}
person Jb Evain    schedule 30.11.2009

Вот полная версия правильного ответа. Здесь используется материал из других ответов, но в него включено важное исправление, которое никто не заметил.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace Timwi.ILReaderExample
{
    public class ILReader
    {
        public class Instruction
        {
            public int StartOffset { get; private set; }
            public OpCode OpCode { get; private set; }
            public long? Argument { get; private set; }
            public Instruction(int startOffset, OpCode opCode, long? argument)
            {
                StartOffset = startOffset;
                OpCode = opCode;
                Argument = argument;
            }
            public override string ToString()
            {
                return OpCode.ToString() + (Argument == null ? string.Empty : " " + Argument.Value);
            }
        }

        private Dictionary<short, OpCode> _opCodeList;

        public ILReader()
        {
            _opCodeList = typeof(OpCodes).GetFields().Where(f => f.FieldType == typeof(OpCode)).Select(f => (OpCode) f.GetValue(null)).ToDictionary(o => o.Value);
        }

        public IEnumerable<Instruction> ReadIL(MethodBase method)
        {
            MethodBody body = method.GetMethodBody();
            if (body == null)
                yield break;

            int offset = 0;
            byte[] il = body.GetILAsByteArray();
            while (offset < il.Length)
            {
                int startOffset = offset;
                byte opCodeByte = il[offset];
                short opCodeValue = opCodeByte;
                offset++;

                // If it's an extended opcode then grab the second byte. The 0xFE prefix codes aren't marked as prefix operators though.
                if (opCodeValue == 0xFE || _opCodeList[opCodeValue].OpCodeType == OpCodeType.Prefix)
                {
                    opCodeValue = (short) ((opCodeValue << 8) + il[offset]);
                    offset++;
                }

                OpCode code = _opCodeList[opCodeValue];

                Int64? argument = null;

                int argumentSize = 4;
                if (code.OperandType == OperandType.InlineNone)
                    argumentSize = 0;
                else if (code.OperandType == OperandType.ShortInlineBrTarget || code.OperandType == OperandType.ShortInlineI || code.OperandType == OperandType.ShortInlineVar)
                    argumentSize = 1;
                else if (code.OperandType == OperandType.InlineVar)
                    argumentSize = 2;
                else if (code.OperandType == OperandType.InlineI8 || code.OperandType == OperandType.InlineR)
                    argumentSize = 8;
                else if (code.OperandType == OperandType.InlineSwitch)
                {
                    long num = il[offset] + (il[offset + 1] << 8) + (il[offset + 2] << 16) + (il[offset + 3] << 24);
                    argumentSize = (int) (4 * num + 4);
                }

                // This does not currently handle the 'switch' instruction meaningfully.
                if (argumentSize > 0)
                {
                    Int64 arg = 0;
                    for (int i = 0; i < argumentSize; ++i)
                    {
                        Int64 v = il[offset + i];
                        arg += v << (i * 8);
                    }
                    argument = arg;
                    offset += argumentSize;
                }

                yield return new Instruction(startOffset, code, argument);
            }
        }
    }

    public static partial class Program
    {
        public static void Main(string[] args)
        {
            var reader = new ILReader();
            var module = typeof(Program).Module;
            foreach (var instruction in reader.ReadIL(typeof(Program).GetMethod("Main")))
            {
                string arg = instruction.Argument.ToString();
                if (instruction.OpCode == OpCodes.Ldfld || instruction.OpCode == OpCodes.Ldflda || instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldsflda || instruction.OpCode == OpCodes.Stfld)
                    arg = module.ResolveField((int) instruction.Argument).Name;
                else if (instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Calli || instruction.OpCode == OpCodes.Callvirt)
                    arg = module.ResolveMethod((int) instruction.Argument).Name;
                else if (instruction.OpCode == OpCodes.Newobj)
                    // This displays the type whose constructor is being called, but you can also determine the specific constructor and find out about its parameter types
                    arg = module.ResolveMethod((int) instruction.Argument).DeclaringType.FullName;
                else if (instruction.OpCode == OpCodes.Ldtoken)
                    arg = module.ResolveMember((int) instruction.Argument).Name;
                else if (instruction.OpCode == OpCodes.Ldstr)
                    arg = module.ResolveString((int) instruction.Argument);
                else if (instruction.OpCode == OpCodes.Constrained || instruction.OpCode == OpCodes.Box)
                    arg = module.ResolveType((int) instruction.Argument).FullName;
                else if (instruction.OpCode == OpCodes.Switch)
                    // For the 'switch' instruction, the "instruction.Argument" is meaningless. You'll need extra code to handle this.
                    arg = "?";
                Console.WriteLine(instruction.OpCode + " " + arg);
            }
            Console.ReadLine();
        }
    }
}
person Community    schedule 30.11.2009
comment
Долго? ибо аргумент не очень элегантный :) - person Jb Evain; 30.11.2009
comment
Я думаю, что это совершенно элегантно. Это необязательное значение. Единственное, что не элегантно, это то, как он пытается (и не может) использовать это поле Argument для аргумента инструкции 'switch', что не помещается в длинное. - person Timwi; 20.12.2009

Вам нужно получить MethodInfo. Вызовите GetMethodBody(), чтобы получить структуру тела метода, а затем вызовите GetILasByteArray для этого. Преобразование этого массива байтов в поток понятного IL.

Грубо говоря

public static List<Instruction> ReadIL(MethodInfo method)
{
    MethodBody body = method.GetMethodBody();
    if (body == null)
        return null;

    var instructions = new List<Instruction>();
    int offset = 0;
    byte[] il = body.GetILAsByteArray();
    while (offset < il.Length)
    {
        int startOffset = offset;
        byte opCodeByte = il[offset];
        short opCodeValue = opCodeByte;
        // If it's an extended opcode then grab the second byte. The 0xFE
        // prefix codes aren't marked as prefix operators though. 
        if (OpCodeList[opCodeValue].OpCodeType == OpCodeType.Prefix
            || opCodeValue == 0xFE)
        {
            opCodeValue = (short) ((opCodeValue << 8) + il[offset + 1]);
            offset += 1;
        }
        // Move to the first byte of the argument.
        offset += 1;

        OpCode code = OpCodeList[opCodeValue];

        Int64? argument = null;
        if (code.ArgumentSize() > 0)
        {
            Int64 arg = 0;
            Debug.Assert(code.ArgumentSize() <= 8);
            for (int i = 0; i < code.ArgumentSize(); ++i)
            {
                Int64 v = il[offset + i];
                arg += v << (i*8);
            }
            argument = arg;
            offset += code.ArgumentSize();
        }

        var instruction = new Instruction(startOffset, code, argument);
        instructions.Add(instruction);
    }

    return instructions;
}

где OpCodeList строится через

OpCodeList = new Dictionary<short, OpCode>();
foreach (var opCode in typeof (OpCodes).GetFields()
                       .Where(f => f.FieldType == typeof (OpCode))
                       .Select(f => (OpCode) f.GetValue(null)))
{
    OpCodeList.Add(opCode.Value, opCode);
}

Затем вы можете определить, какие инструкции являются вызовами свойств IL или поиском переменных-членов или чем-то еще, что вам нужно, и разрешить их с помощью GetType().Module.ResolveField.

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

Изменить: размер аргумента — это метод расширения в OpCode, который просто использует таблицу поиска, чтобы найти подходящее значение.

public static int ArgumentSize(this OpCode opCode)
{
  Dictionary<OperandType, int> operandSizes 
           = new Dictionary<OperandType, int>()
                 {
                    {OperandType.InlineBrTarget, 4},
                    {OperandType.InlineField, 4},
                    {OperandType.InlineI, 4},
                    // etc., etc.
                 };
  return operandSizes[opCode.OperandType];
}

Вы найдете размеры в ECMA 335, которые вы также необходимо просмотреть коды операций, чтобы определить, какие коды операций следует искать, чтобы найти нужные вызовы.

person Ian G    schedule 17.09.2009
comment
Спасибо большое. Код не работает, так как для правильной работы требуется только функция OpCode.ArgumentSize(). Я думаю, это расширение, которое вы написали. - person kerem; 18.09.2009
comment
Большое спасибо за размещение этого кода; это очень полезно. Однако в нем есть ошибка. Размер аргумента инструкции переключения (OperandType.InlineSwitch) не является постоянным, поэтому ваша функция ArgumentSize() не может вернуть правильное значение. Правильное значение — 4*(x+1), где x — 32-разрядное целое число, следующее за кодом операции. - person Timwi; 30.11.2009
comment
В качестве альтернативы вы можете использовать заведомо работающий метод: evain.net/blog/articles/2009/04/30/reflection-based-cil-reader - person Jb Evain; 30.11.2009
comment
Похоже, вы все здесь пытаетесь заново изобрести велосипед. Написание CIL Reader без ошибок — далеко не простая задача. Надеюсь, как сказал Jb Evain, то, чего вы пытаетесь достичь, возможно, используя существующие библиотеки: ILReader, Mono.Cecil и т. д. - person Romain Verdier; 01.12.2009

Reflection — это прежде всего API для проверки метаданных. То, что вы пытаетесь сделать, это проверить необработанный IL, который не является поддерживаемой функцией отражения. Отражение просто возвращает IL как необработанный byte[], который необходимо проверить вручную.

person JaredPar    schedule 16.09.2009
comment
@romkyns, твой комментарий тоже. - person JaredPar; 30.11.2009
comment
Независимо от комментария Ромкинса, ваш ответ действительно не очень подробный. Два других ответа здесь (мой и Jb Evain) имеют полное решение. - person Timwi; 13.02.2010

@Ian G: я составил список из ECMA 335 и обнаружил, что могу использовать

List<MethodInfo> mis = 
    myObject.GetType().GetMethods().Where((MethodInfo mi) =>
        {
            mi.GetCustomAttributes(typeof(MyAttribute), true).Length > 0;
        }
    ).ToList();
foreach(MethodInfo mi in mis)
{
    List<Instruction> lst = ReflectionHelper.ReadIL(mi);
    ... find useful opcode
    FieldInfo fi = mi.Module.ResolveField((int)usefulOpcode.Argument);
    object o = fi.GetValue(myObject);
    ...
}

И список длин опкодов здесь, если кому надо:

Dictionary<OperandType, int> operandSizes
= new Dictionary<OperandType, int>()
{
    {OperandType.InlineBrTarget, 4},
    {OperandType.InlineField, 4},
    {OperandType.InlineI, 4},
    {OperandType.InlineI8,8},
    {OperandType.InlineMethod,4},
    {OperandType.InlineNone,0},
    {OperandType.InlineR,8},
    {OperandType.InlineSig,4},
    {OperandType.InlineString,4},
    {OperandType.InlineSwitch,4},
    {OperandType.InlineTok,4},
    {OperandType.InlineType,4},
    {OperandType.InlineVar,2},
    {OperandType.ShortInlineBrTarget,1},
    {OperandType.ShortInlineI,1},
    {OperandType.ShortInlineR,4},
    {OperandType.ShortInlineVar,1}
};
person kerem    schedule 18.09.2009
comment
В этом есть существенная ошибка; размер операнда для InlineSwitch неверен. Подробности смотрите в моем комментарии к принятому ответу. - person Timwi; 30.11.2009