преобразование .net Func <T> в .net Expression <Func <T>>

118

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

public void GimmeExpression(Expression<Func<T>> expression)
{
    ((MemberExpression)expression.Body).Member.Name; // "DoStuff"
}

public void SomewhereElse()
{
    GimmeExpression(() => thing.DoStuff());
}

Но я бы хотел превратить Func в выражение, только в редких случаях ...

public void ContainTheDanger(Func<T> dangerousCall)
{
    try 
    {
        dangerousCall();
    }
    catch (Exception e)
    {
        // This next line does not work...
        Expression<Func<T>> DangerousExpression = dangerousCall;
        var nameOfDanger = 
            ((MemberExpression)dangerousCall.Body).Member.Name;
        throw new DangerContainer(
            "Danger manifested while " + nameOfDanger, e);
    }
}

public void SomewhereElse()
{
    ContainTheDanger(() => thing.CrossTheStreams());
}

Строка, которая не работает, дает мне ошибку времени компиляции Cannot implicitly convert type 'System.Func<T>' to 'System.Linq.Expressions.Expression<System.Func<T>>'. Явное приведение не разрешает ситуацию. Есть ли средство для этого, которое я не замечаю?

Дэйв Кэмерон
источник
Я не вижу особого смысла в примере «редкий случай». Вызывающий передает Func <T>. Нет необходимости повторять вызывающему объекту, что это был за Func <T> (через исключение).
Адам Ральф
2
Исключение не обрабатывается вызывающей стороной. И поскольку существует несколько сайтов вызова, переданных в разные Func <T> s, перехват исключения в вызывающей стороне создает дублирование.
Дэйв Кэмерон,
1
Трассировка стека исключений предназначена для отображения этой информации. Если исключение выбрасывается при вызове Func <T>, это будет отображаться в трассировке стека. Между прочим, если бы вы выбрали другой путь, т.е. приняли выражение и скомпилировали его для вызова, вы бы потеряли это, поскольку трассировка стека показала бы что-то вроде at lambda_method(Closure )вызова скомпилированного делегата.
Адам Ральф
Думаю, вам стоит посмотреть ответ в этой [ссылка] [1] [1]: stackoverflow.com/questions/9377635/create-expression-from-func/…
Ибрагим Кайс Ибрагим

Ответы:

104

Ох, это совсем непросто. Func<T>представляет собой общее, delegateа не выражение. Если есть какой-либо способ сделать это (из-за оптимизации и других вещей, сделанных компилятором, некоторые данные могут быть выброшены, поэтому может быть невозможно вернуть исходное выражение), он будет дизассемблировать IL на лету и вывести выражение (что отнюдь не просто). Обработка лямбда-выражений как data ( Expression<Func<T>>) - это чудо, совершаемое компилятором (в основном компилятор строит дерево выражений в коде, а не компилирует его в IL).

Связанный факт

Вот почему языки, которые доводят лямбды до крайности (например, Lisp), часто проще реализовать в качестве интерпретаторов . На этих языках код и данные, по сути, одно и то же (даже во время выполнения ), но наш чип не может понять эту форму кода, поэтому мы должны эмулировать такую ​​машину, построив поверх нее интерпретатор, который ее понимает ( выбор, сделанный Lisp-подобными языками) или в некоторой степени жертвуя мощностью (код больше не будет точно соответствовать данным) (выбор, сделанный C #). В C # компилятор создает иллюзию обработки кода как данных, позволяя интерпретировать лямбда-выражения как code ( Func<T>) и data ( Expression<Func<T>>) во время компиляции .

Мехрдад Афшари
источник
3
Лисп не нужно интерпретировать, его легко скомпилировать. Макросы должны быть расширены во время компиляции, и если вы хотите поддержать, evalвам нужно будет запустить компилятор, но в остальном нет никаких проблем с этим.
конфигуратор
2
"Выражение <Func <T>> DangerousExpression = () => dangerousCall ();" нелегко?
mheyman
10
@mheyman Это создаст новую информацию Expressionо вашем действии оболочки, но не будет иметь никакой информации о дереве выражений о внутренних компонентах dangerousCallделегата.
Nenad
34
    private static Expression<Func<T, bool>> FuncToExpression<T>(Func<T, bool> f)  
    {  
        return x => f(x);  
    } 
Override
источник
1
Я хотел пройти по синтаксическому дереву возвращенного выражения. Позволит ли мне такой подход сделать это?
Дэйв Кэмерон
6
@DaveCameron - Нет. См. Ответы выше - уже скомпилированные Funcбудут скрыты в новом выражении. Это просто добавляет один уровень данных поверх кода; вы можете пройти по одному слою, чтобы найти свой параметр fбез дополнительной информации, так что вы там, где начали.
Jonno 01
21

Что вам, вероятно, следует сделать, так это изменить этот метод. Возьмите Expression>, скомпилируйте и запустите. Если это не удается, у вас уже есть выражение, на которое стоит обратить внимание.

public void ContainTheDanger(Expression<Func<T>> dangerousCall)
{
    try 
    {
        dangerousCall().Compile().Invoke();;
    }
    catch (Exception e)
    {
        // This next line does not work...
        var nameOfDanger = 
            ((MemberExpression)dangerousCall.Body).Member.Name;
        throw new DangerContainer(
            "Danger manifested while " + nameOfDanger, e);
    }
}

public void SomewhereElse()
{
    ContainTheDanger(() => thing.CrossTheStreams());
}

Очевидно, вам нужно учитывать влияние этого на производительность и определять, действительно ли это нужно делать.

Дэвид Венгер
источник
7

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

public void ContainTheDanger<T>(Expression<Func<T>> dangerousCall)
{
    try
    {
        var expr = dangerousCall.Compile();
        expr.Invoke();
    }
    catch (Exception e)
    {
        Expression<Func<T>> DangerousExpression = dangerousCall;
        var nameOfDanger = ((MethodCallExpression)dangerousCall.Body).Method.Name;
        throw new DangerContainer("Danger manifested while " + nameOfDanger, e);
    }
}

public void SomewhereElse()
{
    var thing = new Thing();
    ContainTheDanger(() => thing.CrossTheStreams());
}
Стив Уиллкок
источник
6

Если вам иногда нужно выражение, а иногда - делегат, у вас есть 2 варианта:

  • иметь разные методы (по 1 на каждый)
  • всегда принимайте Expression<...>версию, и просто .Compile().Invoke(...)ее, если вам нужен делегат. Очевидно, это дорого.
Марк Гравелл
источник
6

NJection.LambdaConverter - это библиотека, которая преобразует делегатов в выражение

public class Program
{
    private static void Main(string[] args) {
       var lambda = Lambda.TransformMethodTo<Func<string, int>>()
                          .From(() => Parse)
                          .ToLambda();            
    }   

    public static int Parse(string value) {
       return int.Parse(value)
    } 
}
Саги
источник
4
 Expression<Func<T>> ToExpression<T>(Func<T> call)
        {
            MethodCallExpression methodCall = call.Target == null
                ? Expression.Call(call.Method)
                : Expression.Call(Expression.Constant(call.Target), call.Method);

            return Expression.Lambda<Func<T>>(methodCall);
        }
Дмитрий Дзыгин
источник
Не могли бы вы уточнить часть «это не сработает»? Вы действительно пробовали его скомпилировать и запустить? Или в вашем приложении это особо не работает?
Дмитрий Дзыгин
1
FWIW, возможно, главный тикет был не в этом, но мне это было нужно. Это была call.Targetчасть, которая меня убивала. Он работал годами, а потом внезапно перестал работать и начал жаловаться на статический / нестатический бла-бла. В любом случае, спасибо!
Эли Гассерт
-1

+ Изменить

// This next line does not work...
Expression<Func<T>> DangerousExpression = dangerousCall;

к

// This next line works!
Expression<Func<T>> DangerousExpression = () => dangerousCall();
mheyman
источник
Серви, это как абсолютно законный способ самовыражения. синтаксический сахар, чтобы построить его через expression.lambda и expression.call. Как вы думаете, почему он должен давать сбой во время выполнения?
Роман Покровский