【问题标题】:How to get a parameter value passed as a reference with MethodInfo.Invoke when throws an exception in the invoked method当在调用的方法中抛出异常时,如何使用 MethodInfo.Invoke 获取作为引用传递的参数值
【发布时间】:2018-01-30 23:16:08
【问题描述】:

我想知道被调用方法的 out/ref 参数的值是多少。

调用方法时没有抛出异常,在参数中接收到值,但是当调用的方法中抛出异常时,我没有获取到值。直接调用不带Reflection的方法,接收到值。

我做错了什么还是 .net 限制?

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        string[] arguments = new string[] { bool.FalseString, null }; 
        MethodInfo method = typeof(Program).GetMethod("SampleMethod");
        try
        {
            method.Invoke(null, arguments);
            Console.WriteLine(arguments[1]); // arguments[1] = "Hello", Prints Hello
            arguments = new string[] { bool.TrueString, null };
            method.Invoke(null, arguments);
        }
        catch (Exception)
        {
            Console.WriteLine(arguments[1]); // arguments[1] = null, Does not print
        }
        arguments[1] = null;
        try
        {
            SampleMethod(bool.TrueString, out arguments[1]);
        }
        catch (Exception)
        {
            Console.WriteLine(arguments[1]); // arguments[1] = "Hello"
        }
    }

    public static void SampleMethod(string throwsException, out string text)
    {
        text = "Hello";
        if (throwsException == bool.TrueString)
            throw new Exception("Test Exception");
    }
}

搜索了一下之后,我找到了以下解决方案。好用吗?

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class MethodInfoExtension
{
    public static object InvokeStrictly(this MethodInfo source, object obj, object[] parameters)
    {
        ParameterInfo[] paramInfos = source.GetParameters();
        if ((parameters == null) || (paramInfos.Length != parameters.Length))
        {
            throw new ArgumentException();
        }

        Type[] paramTypes = new[] { typeof(object[]) };
        DynamicMethod invokerBuilder = new DynamicMethod(string.Empty, typeof(object), paramTypes);

        ILGenerator ilGenerator = invokerBuilder.GetILGenerator();
        Label exBlockLabel = ilGenerator.BeginExceptionBlock();

        for (int i = 0; i < paramInfos.Length; i++)
        {
            var paramInfo = paramInfos[i];
            bool paramIsByRef = paramInfo.ParameterType.IsByRef;
            var paramType = paramIsByRef ? paramInfo.ParameterType.GetElementType() : paramInfo.ParameterType;

            ilGenerator.DeclareLocal(paramType);

            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Ldc_I4, i);
            ilGenerator.Emit(OpCodes.Ldelem_Ref);
            Label label1 = ilGenerator.DefineLabel();
            ilGenerator.Emit(OpCodes.Brfalse, label1);

            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Ldc_I4, i);
            ilGenerator.Emit(OpCodes.Ldelem_Ref);
            ilGenerator.Emit(OpCodes.Unbox_Any, paramType);
            ilGenerator.Emit(OpCodes.Stloc_S, (byte)i);

            ilGenerator.MarkLabel(label1);

            if (paramIsByRef)
            {
                ilGenerator.Emit(OpCodes.Ldloca_S, (byte)i);
            }
            else
            {
                ilGenerator.Emit(OpCodes.Ldloc_S, (byte)i);
            }
        }

        LocalBuilder resultLocal = ilGenerator.DeclareLocal(typeof(object), false);
        ilGenerator.Emit(OpCodes.Call, source);
        if (source.ReturnType == typeof(void))
        {
            ilGenerator.Emit(OpCodes.Ldnull);
        }
        ilGenerator.Emit(OpCodes.Stloc_S, resultLocal);
        ilGenerator.Emit(OpCodes.Leave, exBlockLabel);

        ilGenerator.BeginFinallyBlock();
        for (int i = 0; i < paramInfos.Length; i++)
        {
            var paramInfo = paramInfos[i];
            bool paramIsByRef = paramInfo.ParameterType.IsByRef;
            var paramType = paramIsByRef ? paramInfo.ParameterType.GetElementType() : paramInfo.ParameterType;

            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Ldc_I4, i);
            ilGenerator.Emit(OpCodes.Ldloc_S, (byte)i);
            if (paramType.IsValueType)
            {
                ilGenerator.Emit(OpCodes.Box, paramType);
            }
            ilGenerator.Emit(OpCodes.Stelem, typeof(object));
        }
        ilGenerator.EndExceptionBlock();

        ilGenerator.Emit(OpCodes.Ldloc_S, resultLocal);
        ilGenerator.Emit(OpCodes.Ret);

        var invoker = (Func<object[], object>)invokerBuilder.CreateDelegate(typeof(Func<object[], object>));
        return invoker(parameters);
    }
}

public class Program
{
    static void Main()
    {
        object[] args = new object[1];
        try
        {
            MethodInfo targetMethod = typeof(Program).GetMethod("Method");
            targetMethod.InvokeStrictly(null, args);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
            Console.WriteLine();
        }
        Console.WriteLine(args[0]);
        Console.ReadLine();
    }
    public static void Method(out string arg)
    {
        arg = "Hello";
        throw new Exception("Test Exception");
    }
}

【问题讨论】:

    标签: c# reflection


    【解决方案1】:

    简而言之,您没有做错任何事。它是对调用实现的限制。

    在直接调用中使用ref 时,本地值的引用会传递到方法中。使用调用时,出于安全原因,仅当调用未引发异常时,才会制作副本并将其复制回您的本地引用中。


    对于长答案...

    因此,以您的示例为例,我创建了 this fiddle 来查看 IL 代码。这给了我们以下信息:

    .method public hidebysig static void SampleMethod(string throwsException, [out] string& text) cil managed
     {
        // 
        .maxstack  2
        .locals init (bool V_0)
        IL_0000:  nop
        IL_0001:  ldarg.1             // Get argument 2
        IL_0002:  ldstr      "Hello"  // Get string literal
        IL_0007:  stind.ref           // store in reference address
        IL_0008:  ldarg.0
        IL_0009:  ldsfld     string [mscorlib]System.Boolean::TrueString
        IL_000e:  call       bool   [mscorlib]System.String::op_Equality(string, string)
        IL_0013:  ldc.i4.0
        IL_0014:  ceq
        IL_0016:  stloc.0
        IL_0017:  ldloc.0
        IL_0018:  brtrue.s   IL_0025
    
        IL_001a:  ldstr      "Test Exception"
        IL_001f:  newobj     instance void [mscorlib]System.Exception::.ctor(string)
        IL_0024:  throw
    
        IL_0025:  ret
    } // end of method Program::SampleMethod
    

    正如预期的那样,“Hello”的值在第二个(输出)参数的 reference 地址中设置。这意味着抛出的异常对设置值没有影响。

    现在使用调用没有直接调用。我没有查找这部分的 IL 代码,但源代码足以弄清楚发生了什么。首先调用Invoke 方法:

    public override Object Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
    {
        object[] arguments = InvokeArgumentsCheck(obj, invokeAttr, binder, parameters, culture);    
        // [Security Check omitted for readability]   
        return UnsafeInvokeInternal(obj, parameters, arguments);
    }
    

    注意,它调用InvokeArgumentsCheck,它返回一个名为arguments的值数组。方法实现如下:

    internal Object[] CheckArguments(Object[] parameters, Binder binder, BindingFlags invokeAttr, CultureInfo culture, Signature sig)
    {
        // copy the arguments in a different array so we detach from any user changes 
        Object[] copyOfParameters = new Object[parameters.Length];
        // [Code omitted for readability]
        for (int i = 0; i < parameters.Length; i++)
        {
            // [Code omitted for readability]
            copyOfParameters[i] = argRT.CheckValue(arg, binder, culture, invokeAttr);
        }
    
        return copyOfParameters;
    }
    

    该方法基本上创建了您指定的输入参数的副本(具有各种类型检查)。从方法中的注释可以看出,这样做是为了防止用户所做的任何更改影响数据将被调用。

    最后我们查看UnsafeInvokeInternal。方法源码如下:

    private object UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
    {
        if (arguments == null || arguments.Length == 0)
            return RuntimeMethodHandle.InvokeMethod(obj, null, Signature, false);
        else
        {
            Object retValue = RuntimeMethodHandle.InvokeMethod(obj, arguments, Signature, false);
    
            // copy out. This should be made only if ByRef are present.
            for (int index = 0; index < arguments.Length; index++)
                parameters[index] = arguments[index];
    
            return retValue;
        }
    }
    

    由于我们有论据,我们可以专注于“其他”部分。通过传递 arguments 来调用该方法,正如我们之前确定的,它是所提供参数的副本。调用完成后,参数值被推回到源数组“Parameters”中。

    在异常情况下,这意味着代码在可以将“Hello”“推回”到我们的输出参数之前被中止。很可能(但我无法检查)它确实改变了参数数组中的复制值,我们只是无法访问它。

    我会让你决定这是出于设计、疏忽,还是他们只是认为无论如何都不应该有这样的用例。

    【讨论】:

    • 谢谢@RMH。我编辑了提出解决方案的问题。好用吗?
    • @AndersonVasconcelosPires 首先让我指出我对 Emit 的经验并不丰富(您可以在 codereview 上试试运气)。快速检查并没有在代码中显示任何太奇怪的东西,只需注意该示例仅适用于静态方法。总而言之,在使用此实现之前,我真的会先查看您的用例,因为它是一种创建神秘错误的简单方法,但是如果您 必须 使用此构造,它似乎是一个不错的解决方案.只要确保你的测试是广泛的。
    猜你喜欢
    • 1970-01-01
    • 2012-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-18
    • 2011-01-07
    相关资源
    最近更新 更多