Можете ли вы получить Func <T>(или аналогичный) из объекта MethodInfo? - PullRequest
56 голосов
/ 29 мая 2010

Я понимаю, что, вообще говоря, использование отражения зависит от производительности. (На самом деле, я вообще не фанат размышлений; это чисто академический вопрос.)

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

public class MyClass {
    public string GetName() {
        return "My Name";
    }
}

Потерпи меня здесь. Я знаю, что если у меня есть экземпляр MyClass с именем x, я могу позвонить x.GetName(). Кроме того, я могу установить для переменной Func<string> значение x.GetName.

Теперь вот мой вопрос. Допустим, я не знаю, что вышеприведенный класс называется MyClass; У меня есть какой-то объект, x, но я понятия не имею, что это такое. Я мог бы проверить, есть ли у этого объекта метод GetName, выполнив это:

MethodInfo getName = x.GetType().GetMethod("GetName");

Предположим, getName не равно нулю. Тогда я не мог бы, кроме того, проверить, если getName.ReturnType == typeof(string) и getName.GetParameters().Length == 0, и на этом этапе я не был бы совершенно уверен, что метод, представленный моим getName объектом, может определенно быть приведен к Func<string>, как-то?

Я понимаю, что есть MethodInfo.Invoke, и я также понимаю, что всегда могу создать Func<string>, например:

Func<string> getNameFunc = () => getName.Invoke(x, null);

Полагаю, я спрашиваю, есть ли способ перейти с MethodInfo объекта на фактического метода, который он представляет, что влечет за собой снижение производительности отражения в process , но после , когда эта точка может вызывать метод напрямую (например, через Func<string> или что-то подобное) без потери производительности.

То, что я представляю, может выглядеть примерно так:

// obviously this would throw an exception if GetActualInstanceMethod returned
// something that couldn't be cast to a Func<string>
Func<string> getNameFunc = (Func<string>)getName.GetActualInstanceMethod(x);

(я понимаю, что этого не существует; мне интересно, есть ли что-нибудь как это.)

Ответы [ 6 ]

37 голосов
/ 29 мая 2010

Этот вид заменяет мой предыдущий ответ, потому что этот, хотя и немного более длинный маршрут, дает вам быстрый вызов метода и, в отличие от некоторых других ответов, позволяет вам проходить через разные случаи (в случае, если вы собираетесь встретить несколько экземпляров одного типа). Если вы этого не хотите, проверьте мое обновление внизу (или посмотрите ответ Бена М).

Вот метод теста, который делает то, что вы хотите:

public class TestType
{
  public string GetName() { return "hello world!"; }
}

[TestMethod]
public void TestMethod2()
{
  object o = new TestType();

  var input = Expression.Parameter(typeof(object), "input");
  var method = o.GetType().GetMethod("GetName", 
    System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
  //you should check for null *and* make sure the return type is string here.
  Assert.IsFalse(method == null && !method.ReturnType.Equals(typeof(string)));

  //now build a dynamic bit of code that does this:
  //(object o) => ((TestType)o).GetName();
  Func<object, string> result = Expression.Lambda<Func<object, string>>(
    Expression.Call(Expression.Convert(input, o.GetType()), method), input).Compile();

  string str = result(o);
  Assert.AreEqual("hello world!", str);
}

Как только вы создадите делегат один раз - вы можете кэшировать его в словаре:

Dictionary<Type, Func<object, string>> _methods;

Все, что вы тогда делаете, это добавляете его в словарь, используя тип входящего объекта (из GetType ()) в качестве ключа. В будущем вы сначала проверяете, есть ли в словаре готовый делегат (и вызываете его, если так), в противном случае сначала создайте его, добавьте, а затем вызовите.

Между прочим, это очень сильно упрощенная версия того, что делает DLR для своего механизма динамической диспетчеризации (в терминах C # именно тогда вы используете ключевое слово «dynamic»).

И наконец

Если, как упоминали несколько человек, вы просто хотите испечь Func, связанный непосредственно с объектом, который вы получаете, тогда вы делаете это:

[TestMethod]
public void TestMethod3()
{
  object o = new TestType();

  var method = o.GetType().GetMethod("GetName", 
    System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);

  Assert.IsFalse(method == null && !method.ReturnType.Equals(typeof(string)));

  //this time, we bake Expression.Constant(o) in.
  Func<string> result = Expression.Lambda<Func<string>>(
   Expression.Call(Expression.Constant(o), method)).Compile();

  string str = result(); //no parameter this time.
  Assert.AreEqual("hello world!", str);
}

Обратите внимание, что, как только дерево выражений будет выброшено, вам нужно убедиться, что o остается в области видимости, иначе вы можете получить некоторые неприятные результаты. Самый простой способ - сохранить локальную ссылку (возможно, в экземпляре класса) на время жизни вашего делегата. (Удалено в результате комментариев Бена М.)

16 голосов
/ 29 мая 2010

Да, это возможно:

Func<string> func = (Func<string>)
                     Delegate.CreateDelegate(typeof(Func<string>), getName);
14 голосов
/ 29 мая 2010

Вот мой ответ, построив дерево выражений. В отличие от других ответов, результат (getNameFunc) является функцией, которая связана с исходным экземпляром - без необходимости передавать его в качестве параметра.

class Program
{
    static void Main(string[] args)
    {
        var p = new Program();
        var getNameFunc = GetStringReturningFunc(p, "GetName");
        var name = getNameFunc();
        Debug.Assert(name == p.GetName());
    }

    public string GetName()
    {
        return "Bob";
    }

    static Func<string> GetStringReturningFunc(object x, string methodName)
    {
        var methodInfo = x.GetType().GetMethod(methodName);

        if (methodInfo == null ||
            methodInfo.ReturnType != typeof(string) ||
            methodInfo.GetParameters().Length != 0)
        {
            throw new ArgumentException();
        }

        var xRef = Expression.Constant(x);
        var callRef = Expression.Call(xRef, methodInfo);
        var lambda = (Expression<Func<string>>)Expression.Lambda(callRef);

        return lambda.Compile();
    }
}
9 голосов
/ 29 мая 2010

Самый простой способ сделать это через Delegate.CreateDelegate:

Func<string> getNameFunc = (Func<string>) Delegate.CreateDelegate(
                                           typeof(Func<string>), x, getName);

Обратите внимание, что это связывает getNameFunc с x, поэтому для каждого x вам необходимо создать новый экземпляр делегата. Эта опция намного менее сложна, чем примеры на основе Expression. Однако с примерами на основе выражений можно создать Func<MyClass, string> getNameFuncForAny один раз, который вы можете использовать для каждого экземпляра MyClass.

Чтобы создать такой getNameFuncForAny, вам понадобится метод, подобный

public Func<MyClass, string> GetInstanceMethod(MethodInfo method)
{
    ParameterExpression x = Expression.Parameter(typeof(MyClass), "it");
    return Expression.Lambda<Func<MyClass, string>>(
        Expression.Call(x, method), x).Compile();
}

который вы можете использовать следующим образом:

Func<MyClass, string> getNameFuncForAny = GetInstanceMethod(getName);

MyClass x1 = new MyClass();
MyClass x2 = new MyClass();

string result1 = getNameFuncForAny(x1);
string result2 = getNameFuncForAny(x2);

Если вы не хотите привязываться к Func<MyClass, string>, вы можете определить

public TDelegate GetParameterlessInstanceMethod<TDelegate>(MethodInfo method)
{
    ParameterExpression x = Expression.Parameter(method.ReflectedType, "it");
    return Expression.Lambda<TDelegate>(
        Expression.Call(x, method), x).Compile();
}
1 голос
/ 29 мая 2010

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

В качестве альтернативы, я написал этот метод довольно давно, основываясь на замечательной статье MSDN, в которой генерируется оболочка, использующая IL для вызова любого MethodInfo способа быстрее, чем с MethodInfo.DynamicInvoke, поскольку после того, как код генерируется, накладных расходов почти нет при обычном вызове.

0 голосов
/ 29 мая 2010

Один из лучших вариантов моего подхода - использовать динамику. Вы могли бы тогда что-то вроде этого:

if( /* This method can be a Func<string> */)
{
    dynamic methodCall = myObject;
    string response = methodCall.GetName();
}
...