首页 > 解决方案 > 编译器如何将返回值转换为返回任务在异步方法中?

问题描述

我设计了以下方法来创建记录。

public Task<Guid> NotAwaited()
{
  Account account = new Account();
  Context.Accounts.Add(account);
  Context.SaveChangesAsync();
  return new Task<Guid>(() => account.Id);
}

然后,我意识到在归还 guid 时存在保存未完成的风险。所以我添加了await,这需要我用 . 装饰方法签名async。在那之后,我收到一个错误,要求使用更简单的语法来返回返回的内容,就像这样。

public async Task<Guid> Awaited()
{
  Account account = new Account();
  Context.Accounts.Add(account);
  await Context.SaveChangesAsync();
  return account.Id;
}

我知道该account.Id部分以某种方式转换为任务。我只是不确定如何。感觉就像它是黑魔法(我理解它不是)。

有隐式转换吗?还是我仍然不正确地执行异步调用?

标签: c#.netasynchronous.net-coreasync-await

解决方案


感觉就像是黑魔法

恐怕已经到了与魔法无法区分的程度了

您编写 C# 代码,然后编译器将其拆分为一起运行的片段,并创建一个状态机,该状态机在每次等待的异步任务完成时“向前移动”。如果您调试代码,调试器知道如何在调试器中表示“局部变量”(实际上可能是状态机类型上的实例成员)并映射到原始源代码的行。

因此,对于您的情况,代码可能类似于(通过 sharplab 创建,请参阅此要点):

[AsyncStateMachine(typeof(<Awaited>d__0))]
public Task<Guid> Awaited()
{
    <Awaited>d__0 stateMachine = default(<Awaited>d__0);
    stateMachine.<>t__builder = AsyncTaskMethodBuilder<Guid>.Create();
    stateMachine.<>1__state = -1;
    AsyncTaskMethodBuilder<Guid> <>t__builder = stateMachine.<>t__builder;
    <>t__builder.Start(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <Awaited>d__0 : IAsyncStateMachine
{
    public int <>1__state;

    public AsyncTaskMethodBuilder<Guid> <>t__builder;

    private Account <account>5__2;

    private TaskAwaiter <>u__1;

    private void MoveNext()
    {
        int num = <>1__state;
        Guid id;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                <account>5__2 = new Account();
                Context.Accounts.Add(<account>5__2);
                awaiter = Context.SaveChangesAsync().GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    num = (<>1__state = 0);
                    <>u__1 = awaiter;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
                    return;
                }
            }
            else
            {
                awaiter = <>u__1;
                <>u__1 = default(TaskAwaiter);
                num = (<>1__state = -1);
            }
            awaiter.GetResult();
            id = <account>5__2.Id;
        }
        catch (Exception exception)
        {
            <>1__state = -2;
            <>t__builder.SetException(exception);
            return;
        }
        <>1__state = -2;
        <>t__builder.SetResult(id);
    }

    void IAsyncStateMachine.MoveNext()
    {
        //ILSpy generated this explicit interface implementation from .override directive in MoveNext
        this.MoveNext();
    }

    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine)
    {
        <>t__builder.SetStateMachine(stateMachine);
    }

    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
        this.SetStateMachine(stateMachine);
    }
}

您可以看到SaveChangesAsync()被调用方位于与访问 Id 属性的位置不同的逻辑分支中。局部变量account现在是<account>5__2生成的结构上的字段。使用的标识符名称实际上都不是有效的 C# 标识符,但在编译为的底层 IL 语言中是有效的,上面的代码是实际生成的代码的反编译 C#-ish 表示。

对该方法的调用Awaited()实际上将创建“隐藏”<Awaited>d__0结构的新实例(在调试模式下,它将是 aclass而不是 astruct以支持编辑和继续)并使用异步基础结构的类型来连接此状态机并运行它.

MoveNext()在启动状态机时调用,而且每次等待的任务完成(作为延续)时都会调用。您可以看到最后一部分将结果设置为id值,这基本上是您的return声明。

值得一提的是,在大多数将异常包装到任务结果中的代码周围还有一个 try-catch - 因此您可以throw在您的代码(或从您的代码中调用的代码)中创建一个失败的任务而不是一个调度方法的异步部分时未处理的异常。


推荐阅读