异步lambda表达式转换为Expression<Func<Task>>。

14

众所周知,我可以将普通的Lambda表达式转换为Expression<T>

Func<int> foo1 = () => 0; // delegate compiles fine
Expression<Func<int>> foo2 = () => 0; // expression compiles fine

我该如何使用异步lambda做同样的事情?我尝试了以下类比:

Func<Task<int>> bar1 = async () => 0; // also compiles (async lambda example)
Expression<Func<Task<int>>> bar2 = async () => 0; // CS1989: Async lambda expressions cannot be converted to expression trees

有什么解决方法吗?


你不会使用Mono.Net吧? - Cory
我在最新发布的VS2015中使用C# 6编译器时遇到了这个错误。我认为在C# 5和/或Mono编译器上情况不会改变。如果有改变,请告诉我。 - ForNeVeR
4个回答

17
C#只能将Lambda表达式转换为表达式树,前提是代码可以用表达式树表示。如果你注意到的话,System.Linq.Expressions中没有"async"关键字的等效项。
因此,不仅是"async",而是任何在C#中没有提供表达式等效项的内容,都无法被转换为表达式树。
其他例子包括:
  1. lock
  2. unsafe
  3. using
  4. yield
  5. await

1
除了 awaityield break / return 之外,还有哪些类似的语言结构的例子? - ForNeVeR

7
这个错误的意思很明显:

"异步lambda表达式无法转换为表达式树"

它在Async/Await FAQ中也有记录。而且,async-await是框架之上的编译器功能,表达式用于将代码转换为其他命令(如SQL)。这些其他语言可能没有async-await的等效物,因此通过表达式启用它似乎不值得。所以,我认为没有解决方法。

2
哇,这是一个有趣的链接。不知道我怎么会错过那个常见问题解答。 - ForNeVeR

1
编辑:我创建了一个库,实现将普通表达式树转换为异步表达式树。它不是使用专门的类型来存储状态,而是使用外部lambda的闭包,并使用每个awaiter类型一个变量(类似于C#状态机)来实现。

https://github.com/avonwyss/bsn.AsyncLambdaExpression


“确实可以实现异步表达式树,但目前没有框架支持构建异步表达式树(还没有吗?)。因此,这绝对不是一个简单的任务,但我已经有几个在日常生产中使用的实现。”
“所需的材料如下:”
TaskCompletionSource派生的帮助器类,用于提供任务及其所有相关内容。
我们需要添加一个“State”属性(您可以使用不同的名称,但这与由C#编译器为async-await生成的帮助器对齐),以跟踪状态机当前所处的状态。
然后,我们需要有一个“MoveNext”属性,它是一个“Action”。 这将被调用以处理状态机的下一个状态。
最后,我们需要一个地方来存储当前待处理的“Awaiter”,这将是一个对象类型的属性。
异步方法通过使用“SetResult”,“SetException”(或“SetCanceled”)之一终止。
这样的实现可能如下所示:
internal class AsyncExpressionContext<T>: TaskCompletionSource<T> {
    public int State {
        get;
        set;
    }

    public object Awaiter {
        get;
        set;
    }

    public Action MoveNext {
        get;
    }

    public AsyncExpressionContext(Action<AsyncExpressionContext<T>> stateMachineFunc): base(TaskCreationOptions.RunContinuationsAsynchronously) {
        MoveNext = delegate {
            try {
                stateMachineFunc(this);
            }
            catch (Exception ex) {
                State = -1;
                Awaiter = null;
                SetException(ex);
            }
        };
    }
}

一个状态机的lambda表达式,它将实际的状态机实现为switch语句,类似于这样(不能直接编译,但应该能够了解需要完成的内容):
var paraContext = Expression.Parameter(AsyncExpressionContext<T>, "context");
var stateMachineLambda = Expression.Lambda<Action<AsyncExpressionContext<T>>>(Expression.Block(new[] { varGlobal },
    Expression.Switch(typeof(void),
        Expression.Property(paraContext, nameof(AsyncExpressionContext<T>.State)),
        Expression.Throw(
            Expression.New(ctor_InvalidOperationException, Expression.Constant("Invalid state"))),
    null,
    stateMachineCases));

每个案例都实现了状态机的一个状态。我不会详细介绍异步等待状态机概念,因为有很多优秀的资源可用,特别是许多博客文章,它们详细解释了一切。

https://devblogs.microsoft.com/premier-developer/dissecting-the-async-methods-in-c/

通过利用标签和goto表达式(如果它们不携带值,则可以跨越块跳转),可以在异步方法被调用后同步返回时实现“热路径优化”。
基本概念如下(伪代码):
State 0 (start state):
  - Initiate async call, which returns an awaitable object.
  - Optionally and if present call ConfigureAwait(false) to get another awaiter.
  - Check the IsCompleted property of the awaiter.
    - If true, call GetResult() on the awaiter and store the the result in a "global" variable, then jump to the label "state0continuation"
    - If false, store the awaiter and the next state in the context object, then call OnCompleted(context.MoveNext) on the awaiter and return

State X (continuation states):
  - Cast the awaiter from the context object back to its original type and call GetResult(), store its result in the same "global" variable.
  - Label "state0continuation" goes here; if the call was synchronous we already have our value in the "global" variable
  - Do some non-async work
  - To end the async call, call SetResult() on the context and return (setting the state property to an invalid value and clearing the awaiter property may be a good idea for keeping things tidy)
  - You can make other async calls just as shown in state 0 and move to other states

一个“引导程序”表达式,它创建TaskCompletionSource并启动状态机。这就是将作为异步lambda公开的内容。当然,您也可以添加参数,通过闭包传递它们或将它们添加到上下文对象中。
var varContext = Expression.Variable(typeof(AsyncExpressionContext<T>), "context");
var asyncLambda = Expression.Lambda<Func<Task<T>>>(
    Expression.Block(
        Expression.Assign(
            varContext,
            Expression.New(ctor_AsyncExpressionContext,
                Expression.Lambda<Action<AsyncExpressionContext<T>>>(
                    stateMachineExression,
                    paraContext))),
    Expression.Invoke(
        Expression.Property(varContext, nameof(AsyncExpressionContext<T>.MoveNext)),
        varContext),
    Expression.Property(varContext, nameof(AsyncExpressionContext<T>.Task)));

这基本上就是线性异步方法所需的全部内容。如果您想添加条件分支,为了使流程正确地进入下一个状态,需要做一些更棘手的事情,但同样也是可能的并且可行的。

1

(迟到的答案)

您可以将此代码重写为:

Expression<Func<Task<int>>> bar2 = () => Task.FromResult(0);

现在,您可以创建返回Task的委托:

var result = bar2.Compile();

并等待它:

await result.Invoke();

我知道这只是一个简单的例子,但可能可以不使用await - 使用Task.ContinueWith()或类似的东西:

Expression<Func<Task<int>>> moreComplex = () => 
    SomeAsyncOperation() // can't be awaited as lambda is not marked as async
        .ContinueWith(completedTask => /* continuationLogic */)
        .Unwrap(); // to get unwrapped task instead of Task<Task>

您不能在Expression中使用async/await(因为它是编译器的内容),但是您可以使用返回Task的方法/委托(这是常规方法,可以等待)。然后编译器可以创建需要的内容以等待委托调用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接