【问题标题】:How can I get fields used in a method (.NET)?如何获取方法(.NET)中使用的字段?
【发布时间】:2009-09-16 21:07:23
【问题描述】:

在 .NET 中,如何使用反射获取方法中使用的类变量?

例如:

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 变量。 我所说的变量是指该特定变量的值和/或类型和构造函数参数。

【问题讨论】:

  • 我不明白你在做什么。为什么需要反思?对于“类变量”,您是指字段吗?您可以轻松获取某个字段的当前实例,但不能获取用于创建它的构造函数参数。为什么需要这个?
  • 类变量是指类范围字段,即类。我正在考虑为某些方法声明一个属性,这些方法需要根据它从其父类中使用的变量来完成特殊的事情。某个领域的当前实例可以为我工作。

标签: .net reflection methodinfo


【解决方案1】:

有很多不同的答案,但没有一个能吸引我,这里是我的。它正在使用我的Reflection based IL reader

这是一个检索方法使用的所有字段的方法:

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

【讨论】:

    【解决方案2】:

    这是正确答案的完整版本。这使用了其他答案中的材料,但包含了一个没有其他人发现的重要错误修复。

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

    【讨论】:

    • 长?因为这个论点不是很优雅:)
    • 我认为它非常优雅。它是一个可选值。唯一不优雅的是它如何尝试(并且失败)将该 Argument 字段用于“switch”指令的参数,这不适合很长时间。
    【解决方案3】:

    您需要获取 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 中找到大小,您还需要查看 OpCodes 以查找要搜索的 OpCodes 以找到您正在寻找的呼叫。

    【讨论】:

    • 非常感谢。代码不起作用,因为它只需要 OpCode.ArgumentSize() 函数才能正常工作。我认为那是你写的扩展。
    • 非常感谢您发布此代码;它非常有用。但是,它有一个错误。 switch 指令 (OperandType.InlineSwitch) 的参数大小不是恒定的,因此您的 ArgumentSize() 函数无法返回正确的值。正确的值是 4*(x+1),其中 x 是操作码后面的 32 位整数。
    • 或者,您可以使用已知有效的方法:evain.net/blog/articles/2009/04/30/reflection-based-cil-reader
    • 看来你们都想在这里重新发明轮子。编写一个没有错误的 CIL 阅读器绝非易事。希望正如 Jb Evain 所说,您想要实现的目标是利用现有库:ILReader、Mono.Cecil 等。
    【解决方案4】:

    Reflection 主要是一种用于检查元数据的 API。您要做的是检查原始 IL,这不是反射的受支持功能。反射只是将 IL 作为原始字节 [] 返回,必须手动检查。

    【讨论】:

    • @romkyns,你的评论也不是。
    • 不管romkyns的评论,你的回答确实不是很详细。这里的另外两个答案(我的和 Jb Evain 的)有一个完整的解决方案。
    【解决方案5】:

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

    【讨论】:

    • 这里有一个重大错误; InlineSwitch 的操作数大小错误。有关详细信息,请参阅我对已接受答案的评论。
    猜你喜欢
    • 1970-01-01
    • 2021-11-29
    • 1970-01-01
    • 1970-01-01
    • 2016-05-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-06
    相关资源
    最近更新 更多