Как создать Expression.Lambda, когда тип неизвестен до выполнения?

Это лучше всего объяснить с помощью кода. У меня есть общий класс, который имеет метод, который возвращает целое число. Вот простая версия для объяснения...

public class Gen<T>
{
    public int DoSomething(T instance)
    {
        // Real code does something more interesting!
        return 1;
    }
}

Во время выполнения я использую отражение, чтобы обнаружить тип чего-то, а затем хочу создать экземпляр класса Gen для этого конкретного типа. Это достаточно легко и сделано так...

Type fieldType = // This is the type I have discovered
Type genericType = typeof(Gen<>).MakeGenericType(fieldType);
object genericInstance = Activator.CreateInstance(genericType);

Теперь я хочу создать выражение, которое примет в качестве параметра экземпляр типа generic, а затем вызовет метод DoSomething этого типа. Поэтому я хочу, чтобы выражение эффективно выполняло это...

int answer = genericInstance.DoSomething(instance);

... за исключением того, что у меня нет "экземпляра" до некоторой точки позже во время выполнения, а genericInstance - сгенерированный тип, как видно выше. Моя попытка создать Лямбду для этого заключается в следующем...

MethodInfo mi = genericType.GetMethod("DoSomething", 
                                      BindingFlags.Instance | BindingFlags.Public);

var p1 = Expression.Parameter(genericType, "generic");
var p2 = Expression.Parameter(fieldType, "instance");

var x = Expression.Lambda<Func<genericType, fieldType, int>>
            (Expression.Call(p1, mi, p2), 
             new[] { p1, p2 }).Compile();

... так что позже я могу назвать это чем-то вроде этого...

int answer = x(genericInstance, instance);

Конечно, вы не можете предоставить Func с параметрами экземпляра, и поэтому я понятия не имею, как параметризовать генерацию Lambda. Любые идеи?

Ответ 1

Я думаю, вы просто использовали бы Expression.Lambda, который будет использовать тип делегата как тип, а не как общий, и создайте свой Func на лету, как будто вы с Gen<>:

MethodInfo mi = genericType.GetMethod("DoSomething",
                                BindingFlags.Instance | BindingFlags.Public);

var p1 = Expression.Parameter(genericType, "generic");
var p2 = Expression.Parameter(fieldType, "instance");
var func = typeof (Func<,,>);
var genericFunc = func.MakeGenericType(genericType, fieldType, typeof(int));
var x = Expression.Lambda(genericFunc, Expression.Call(p1, mi, p2),
                new[] { p1, p2 }).Compile();

Это вернет делегат, а не строго типизированный Func, но вы можете, конечно, бросить его, если это необходимо (и, казалось бы, сложно, если вы не знаете, что вы делаете), или динамически вызывать его с помощью DynamicInvoke на нем.

int answer = (int) x.DynamicInvoke(genericInstance, instance);

ИЗМЕНИТЬ

Хорошая идея, которая действительно работает. К сожалению, причина, по которой я хочу использовать сильно типизированную скомпилированную Lambda, - это производительность. Использование DynamicInvoke довольно медленное по сравнению с типизированной Lambda.

Кажется, что это работает без необходимости динамического вызова.

var p1 = Expression.Parameter(genericType, "generic");
var p2 = Expression.Parameter(fieldType, "instance");
var func = typeof(Func<,,>);
var genericFunc = func.MakeGenericType(genericType, fieldType, typeof(int));
var x = Expression.Lambda(genericFunc, Expression.Call(p1, mi, p2), new[] { p1, p2 });
var invoke = Expression.Invoke(x, Expression.Constant(genericInstance), Expression.Constant(instance));
var answer = Expression.Lambda<Func<int>>(invoke).Compile()();

РЕДАКТИРОВАТЬ 2:

Очень упрощенная версия:

Type fieldType = ;// This is the type I have discovered
Type genericType = typeof(Gen<>).MakeGenericType(fieldType);
object genericInstance = Activator.CreateInstance(genericType);
MethodInfo mi = genericType.GetMethod("DoSomething",
                                BindingFlags.Instance | BindingFlags.Public);
var value = Expression.Constant(instance, fieldType);
var lambda = Expression.Lambda<Func<int>>(Expression.Call(Expression.Constant(genericInstance), mi, value));
var answer = lambda.Compile()();

Ответ 2

Этот ответ применяется только в том случае, если вы используете .NET 4.0.

Если вы создаете genericInstance dynamic вместо object, вы можете вызвать метод DoSomething на нем напрямую, а время выполнения динамического языка позаботится обо всем вам.

class Type1 {
    public int DoSomething() { return 1; }
}
class Type2 {
    public int DoSomething() { return 2; }
}

static void TestDynamic() {
    dynamic t1 = Activator.CreateInstance(typeof(Type1));
    int answer1 = t1.DoSomething(); // returns 1

    dynamic t2 = Activator.CreateInstance(typeof(Type2));
    int answer2 = t2.DoSomething(); // returns 2
}

Если вам нужно сохранить эту структуру класса (Gen<T>), то я не вижу простого способа избежать того, что во время компиляции вы не знаете тип T. Если вы хотите вызвать делегата, вам нужно либо узнать его полный тип во время компиляции, либо вам нужно передать параметры как объекты.

Использование dynamic позволяет вам скрывать сложность получения MethodInfo и т.д. и дает отличную производительность. Единственный недостаток против DynamicInvoke, который я вижу, заключается в том, что я считаю, что вы получаете первоначальные накладные расходы на разрешение динамического вызова один раз для каждого сайта вызова. Связывание кэшируется так, что они выполняются очень быстро со второго раза, если вы вызываете его на объекты с тем же типом.

Ответ 3

Лучше принять object и использовать convert для известного типа.

Вот пример, как построить доступ к свойству по имени на неизвестной глубине:

var model = new { A = new { B = 10L } };
string prop = "A.B";
var parameter = Expression.Parameter(typeof(object));
Func<object, long> expr = (Func<object, long>) Expression.Lambda(prop.Split('.').Aggregate<string, Expression>(Expression.Convert(parameter, model.GetType()), Expression.Property), parameter).Compile();
expr(model).Dump();

Это позволяет избежать дополнительных затрат на DynamicInvoke, когда тип делегата неизвестен во время компиляции.