invoking correct generic method using Type variabl

2019-08-12 20:15发布

问题:

static class Example
{
    public static string Method<T>(ref List<string> p2, out string p3, string p4)
    {
      ...
    }
    public static string Method<T>(ref List<string> p2, out string p3, int p4)
    {
      ...
    }
}

The following obviously doesn't work, but that's the idea:

public static string MethodCaller(Type theType, ref List<string> p2, out string p3, string p4)
{
    Method<theType>(ref p2, out p3, p4);
}

Using GetMethod? how does it know which of the two overloaded methods to use? should we Use Expression.Call instead? how do we deal with the ref and out parameters?

Please help :)

回答1:

This can be done through reflection, although finding the correct overload is a bit messy:

class Program
{
    static void Main(string[] args)
    {
        List<string> p2 = new List<string>();
        string p3;
        string p4 = "input string";
        string result = MethodCaller(typeof(DateTime), ref p2, out p3, p4);
    }

    public static string MethodCaller(Type theType, ref List<string> p2, out string p3, string p4)
    {
        MethodInfo method = (from m in typeof(Example).GetMethods()
                             let p = m.GetParameters()
                             where m.Name == "Method"
                             && p.Length == 3
                             && p[0].ParameterType.IsByRef
                             && p[0].ParameterType.HasElementType
                             && p[0].ParameterType.GetElementType() == typeof(List<string>)
                             && p[1].ParameterType.IsByRef
                             && p[1].ParameterType.HasElementType
                             && p[1].ParameterType.GetElementType() == typeof(string)
                             && p[2].ParameterType == typeof(string)
                             select m).Single();
        MethodInfo genericMethod = method.MakeGenericMethod(theType);
        object[] parameters = new object[] { null, null, p4 };
        string returnValue = (string)genericMethod.Invoke(null, parameters);
        p2 = (List<string>)parameters[0];
        p3 = (string)parameters[1];
        return returnValue;
    }
}

static class Example
{
    public static string Method<T>(ref List<string> p2, out string p3, string p4)
    {
        p2 = new List<string>();
        p2.Add(typeof(T).FullName);
        p2.Add(p4);
        p3 = "output string";
        return "return value";
    }

    public static string Method<T>(ref List<string> p2, out string p3, int p4)
    {
        p2 = new List<string>();
        p2.Add(typeof(T).FullName);
        p2.Add(p4.ToString());
        p3 = "output string";
        return "return value";
    }
}


回答2:

I was looking for somehow similar approach, but unfortunately haven't found it - but then decided to solve it on my own. However - during prototyping I've discovered that 'out' and 'ref' are mutually exclusive - so you can support only one of them.

So I wanted to support syntax like:

 object DoCall<A1>( String function, A1 a1 )

This will require to generate functions like:

 object DoCall<A1>( String function, out A1 a1 )
 object DoCall<A1>( String function, ref A1 a1 )

Which compiler will not like.

So I've decided to support only 'ref' keyword - since it can support in and out direction, but 'out' supports only out direction.

But additionally like someone might noticed - if you need to support all kind of parameters permutations - simple coding is not enough - you need to write code generator - what I did at the end.

So test code looks somehow like this:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace TestReflection
{
    public class CustomClassAsArg
    {
        public string MyInfo { get; set; }
    }

    public class CallMe
    {
        public void Hello1(String msg, int i)
        {
            Console.WriteLine(msg + ": " + i.ToString());
        }

        public void Hello2(ref String msg)
        {
            msg += "out string";
        }

        public void Hello2(ref int a)
        {
            a += 2;
        }

        public string Hello3(string a)
        {
            return a + "--";
        }

        public static bool MyStaticMethod( int arg, ref String outs )
        {
            outs = "->" + arg.ToString();
            return true;
        }

        public bool? ThreeStateTest( int i )
        {
            switch ( i )
            {
                case 0:
                    return null;
                case 1:
                    return false;
                case 2:
                    return true;
            }

            return null;
        }

        public void UpdateCC( CustomClassAsArg c )
        {
            c.MyInfo = "updated";
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            ClassCaller.UpdateSourceCodeHelperFunctions(2);

            CallMe m = new CallMe();
            ClassCaller c = new ClassCaller(m);
            string r = "in string ";
            int arg = 1;
            String sx = "";
            object ox = c.DoCall("!MyStaticMethod", 23, ref sx);
            Console.WriteLine(sx);

            c.DoCall("Hello1", "hello world", 1);
            c.DoCall("Hello2", ref r);
            Console.WriteLine(r);
            c.DoCall("Hello2", ref arg);
            Console.WriteLine(arg.ToString());

            bool? rt = (bool?)c.DoCall("ThreeStateTest", 0);
            rt = (bool?)c.DoCall("ThreeStateTest", 1);
            rt = (bool?)c.DoCall("ThreeStateTest", 2);

            CustomClassAsArg ccarg = new CustomClassAsArg();
            c.DoCall("UpdateCC",ccarg);

        } //Main
    }
}

And ClassCaller.cs itself:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

/// <summary>
/// Helper class for performing invoke function call in dynamically loaded assembly.
/// </summary>
public class ClassCaller
{
    Type type;
    object o;
    bool throwOnError;

    /// <summary>
    /// Can specify class using only assembly name / class type without
    /// actual class instance - if you intend to call only static methods.
    /// </summary>
    /// <param name="assemblyName">Assembly name</param>
    /// <param name="className">Class name, including namespace</param>
    /// <param name="_throwOnError">true if throw on error</param>
    public ClassCaller(String assemblyName, String className, bool _throwOnError)
    {
        throwOnError = _throwOnError;
        Assembly asm = AppDomain.CurrentDomain.GetAssemblies().Where(x => x.GetName().Name == assemblyName).FirstOrDefault();
        if (asm == null)
        {
            if (_throwOnError)
                throw new NullReferenceException("Assembly with name '" + assemblyName + "' was not found");

            return;
        }

        type = asm.GetType(className, _throwOnError);
    }

    public ClassCaller(object _o)
    {
        type = _o.GetType();
        o = _o;
    }

    /// <summary>
    /// Gets method to invoke. 
    /// </summary>
    /// <param name="func">Function name to get. Use '!' as a prefix if it's static function.</param>
    /// <param name="types">Function argument types.</param>
    /// <returns>Method to be invoked.</returns>
    public MethodInfo GetFunc(String func, Type[] types)
    {
        bool bIsStatic = func.FirstOrDefault() == '!';
        if (bIsStatic) func = func.Substring(1);

        BindingFlags f = BindingFlags.Public | BindingFlags.NonPublic;
        if (!bIsStatic)
            f |= BindingFlags.Instance;
        else
            f |= BindingFlags.Static;

        MethodInfo m = type.GetMethod(func, f, null, types, null);

        if (m == null && throwOnError)
            throw new NotSupportedException("Compatible function '" + func + "' not found");

        return m;
    }

    //Autogenerated code starts (Do not edit)

    public object DoCall(string func)
    {
        Type[] types = new Type[] { };
        object[] args = new object[] { };
        MethodInfo f = GetFunc(func, types);
        if (f == null)
            return null;
        object r = f.Invoke(o, args);
        return r;
    }

    public object DoCall<A1>(string func, A1 a1)
    {
        Type[] types = new Type[] { typeof(A1) };
        object[] args = new object[] { a1 };
        MethodInfo f = GetFunc(func, types);
        if (f == null)
            return null;
        object r = f.Invoke(o, args);
        return r;
    }

    public object DoCall<A1>(string func, ref A1 a1)
    {
        Type[] types = new Type[] { typeof(A1).MakeByRefType() };
        object[] args = new object[] { a1 };
        MethodInfo f = GetFunc(func, types);
        if (f == null)
            return null;
        object r = f.Invoke(o, args);
        a1 = (A1)args[0];
        return r;
    }

    public object DoCall<A1, A2>(string func, A1 a1, A2 a2)
    {
        Type[] types = new Type[] { typeof(A1), typeof(A2) };
        object[] args = new object[] { a1, a2 };
        MethodInfo f = GetFunc(func, types);
        if (f == null)
            return null;
        object r = f.Invoke(o, args);
        return r;
    }

    public object DoCall<A1, A2>(string func, ref A1 a1, A2 a2)
    {
        Type[] types = new Type[] { typeof(A1).MakeByRefType(), typeof(A2) };
        object[] args = new object[] { a1, a2 };
        MethodInfo f = GetFunc(func, types);
        if (f == null)
            return null;
        object r = f.Invoke(o, args);
        a1 = (A1)args[0];
        return r;
    }

    public object DoCall<A1, A2>(string func, A1 a1, ref A2 a2)
    {
        Type[] types = new Type[] { typeof(A1), typeof(A2).MakeByRefType() };
        object[] args = new object[] { a1, a2 };
        MethodInfo f = GetFunc(func, types);
        if (f == null)
            return null;
        object r = f.Invoke(o, args);
        a2 = (A2)args[1];
        return r;
    }

    public object DoCall<A1, A2>(string func, ref A1 a1, ref A2 a2)
    {
        Type[] types = new Type[] { typeof(A1).MakeByRefType(), typeof(A2).MakeByRefType() };
        object[] args = new object[] { a1, a2 };
        MethodInfo f = GetFunc(func, types);
        if (f == null)
            return null;
        object r = f.Invoke(o, args);
        a1 = (A1)args[0];
        a2 = (A2)args[1];
        return r;
    }


    //Autogenerated code ends

    public static void UpdateSourceCodeHelperFunctions( int nParametersToSupport)
    {
        String srcFilename = new StackTrace(true).GetFrame(0).GetFileName();
        String src = File.ReadAllText(srcFilename, Encoding.UTF8);

        String autogenRegex = "(Autogenerated\\scode\\sstarts.*?[\r\n]{2})(.*)([\r\n]{2}\\s+//Autogenerated\\scode\\sends)";

        if (!Regex.Match(src, autogenRegex, RegexOptions.Singleline).Success)
        {
            Console.WriteLine("Error: Invalid source code");
            return;
        }

        string[] argType = new String[] { "", "ref" };

        String s = "";
        string lf = "\r\n";
        string headSpace = "    ";
        for (int callArgs = 0; callArgs <= nParametersToSupport; callArgs++)
        {
            int[] argTypes = new int[callArgs];
            int iterations = (int)Math.Pow(2, callArgs);
            for (int i = 0; i < iterations; i++)
            {
                //public object DoCall<A1, A2>(String func, A1 a1, A2 a2)
                s += headSpace;
                s += "public object DoCall" + ((callArgs != 0) ? "<" : "");
                s += String.Join(", ", Enumerable.Range(1, callArgs).Select(n => "A" + n));
                s += (callArgs != 0) ? ">" : "";
                s += "(string func";

                String types = "";
                String paramsList = "";

                bool[] isRefType = new bool[callArgs];

                for (int iArg = 0; iArg < callArgs; iArg++)
                {
                    isRefType[iArg] = (((1 << iArg) & i) != 0);
                    String isRef = isRefType[iArg] ? "ref " : "";
                    String argTypeName = "A" + (iArg + 1);
                    String argName = "a" + (iArg + 1);
                    s += ", ";
                    s += isRef;
                    s += argTypeName + " " + argName;

                    if (iArg != 0)
                    {
                        types += ", ";
                        paramsList += ", ";
                    }

                    types += "typeof(" + argTypeName + ")";
                    if (isRefType[iArg])
                        types += ".MakeByRefType()";

                    paramsList += argName;
                } //for

                s += ")";
                s += lf;
                s += headSpace + "{" + lf;

                //Type[] types = new Type[] { typeof(A1).MakeByRefType() };
                s += headSpace + "    ";
                if( types.Length != 0 ) types += " ";
                s += "Type[] types = new Type[] { " + types + "};";
                s += lf;

                //object[] args = new object[] { a1 };
                s += headSpace + "    ";
                if( paramsList.Length != 0 ) paramsList += " ";
                s += "object[] args = new object[] { " + paramsList + "};";
                s += lf;

                //MethodInfo f = GetFunc(func, types);
                //if (f == null)
                //    return null;
                //object r = f.Invoke(o, args);
                s += headSpace + "    MethodInfo f = GetFunc(func, types);" + lf;
                s += headSpace + "    if (f == null)" + lf;
                s += headSpace + "        return null;" + lf;
                s += headSpace + "    object r = f.Invoke(o, args);" + lf;

                for (int iArg = 0; iArg < callArgs; iArg++)
                {
                    if (!isRefType[iArg])
                        continue;
                    // a1 = (A1)args[0];
                    String argTypeName = "A" + (iArg + 1);
                    String argName = "a" + (iArg + 1);

                    s += headSpace + "    ";
                    s += argName + " = (" + argTypeName + ")args[" + iArg + "];";
                    s += lf;
                }

                s += headSpace + "    return r;" + lf;
                s += headSpace + "}" + lf;
                s += lf;
            }

        } //for

        String oldautogenCode = Regex.Match(src, autogenRegex, RegexOptions.Singleline).Groups[2].Value;

        //
        // Visual studio text editor configuration affects spacing. We trim here everything so we can compare output.
        //
        oldautogenCode = oldautogenCode.Replace(" ", "").TrimStart('\r','\n');
        String newautogenCode = s.Replace(" ", "").TrimStart('\r', '\n');

        String newSrc = Regex.Replace(src, autogenRegex, "$1\r\n" + s + "$3", RegexOptions.Singleline);

        if (oldautogenCode == newautogenCode)
        {
            Console.WriteLine("Source code is up-to-date.");
        }
        else
        {
            File.WriteAllText(srcFilename, newSrc, Encoding.UTF8);
        }
    } //UpdateSourceCodeHelperFunctions
} //class ClassCaller

so:

ClassCaller.UpdateSourceCodeHelperFunctions(2);

Function regenerates auto-generated code part to support - for demo purposes I'm supporting now only 2 parameters to be called, but typically you need more parameters - but this will increase autogenerated code size.

Auto-generated code can be updated only in debug mode, not in release configuration. (But this is not needed in release at all).

Maybe this is not direct answer to your question, but I think it follows your idea.

Reflect call requires all argument types to match 100% correctly - otherwise reflection will not be able to find required method.

Also this solution has limitation - for example it won't be able to find right method if some of parameters are optional - like:

void DoMethod( int a, int b = 0 );

So you can call:

DoMethod(5);

But not:

DoCall("DoMethod", 5);

it must be full set of arguments:

DoCall("DoMethod", 5, 0);

I understand that method calling through reflection might be expensive in consumed time wise, so think twice before using it.

Update 31.5.2016 I've also find out that C#'s 'dynamic' keyword can be used also for dynamic invocation of specific method without knowing reflection method call details, but dynamic operates only on instances, static method calls are still easier to make with ClassCaller.