问题
I've designed the following method to create records.
public Task<Guid> NotAwaited()
{
Account account = new Account();
Context.Accounts.Add(account);
Context.SaveChangesAsync();
return new Task<Guid>(() => account.Id);
}
Then, I realized that there's risk of the saving not being finished at the moment of returning the guid. So I've added await
, which required me to decorate the method signature with async
. Upon that, I got an error demanding a simpler syntax of what's being returned, like this.
public async Task<Guid> Awaited()
{
Account account = new Account();
Context.Accounts.Add(account);
await Context.SaveChangesAsync();
return account.Id;
}
I understand that the account.Id
part gets converted to a task somehow. I'm just uncertain how. It feel like if it's black magic (which I understand it isn't).
Is there an implicit conversion? Or am I still performing the asynchronous call improperly?
回答1:
You can think of async
as wrapping results (both return values and exceptions) into a Task<T>
.
Likewise, await
unwraps the results (extracting the return value or raising an exception).
I have an async intro that goes into more detail, and I recommend async best practices as followup to that. On a side note, you should never use the Task constructor.
回答2:
It feel like if it's black magic
It probably is sufficiently advanced to be indistinguishable from magic.
You write C# code that the compiler then splits into pieces that run together and creates a state machine that "moves forward" every time an awaited asynchronous task completes. If you debug your code, the debugger knows how to represent the "local variables" (which may actually be instance members on a state machine type) in the debugger and map to the line of your original source code.
So for your case, the code could look something like (created via sharplab, see this gist):
[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);
}
}
You can see that the party where SaveChangesAsync()
is called is located in a different logical branch than where the access to the Id property is. The local variable account
is now the <account>5__2
field on the generated struct. None of the identifier names used is actually a valid C# identifier, but is valid in the underlying IL language that is compiled to, the above code is a decompiled C#-ish representation of the code that is actually generated.
Calls to the Awaited()
method will actually create new instances of the "hidden" <Awaited>d__0
struct (in debug mode it will be a class
instead of a struct
to support edit-and-continue) and use types of the async infrastructure to wire this state machine up and run it.
MoveNext()
is called when starting the state machine but also each time that an awaited task completes (as a continuation). You can see that the last part sets the result to the id
value, which is basically your return
statement.
Noteworthy to mention is that there is also a try-catch around most of the code which wraps exceptions into the task result - so you can throw
in your code (or code called from your code) and it ends up creating a failed task instead of an unhandled exception when the asynchronous part of the method is scheduled.
来源:https://stackoverflow.com/questions/56587496/how-does-compiler-converts-return-value-into-return-taskvalue-in-async-methods