我有服务方法返回一个等待的成员视图模型。
public async Task<MemberVm> GetMember(Guid id)
{
Task<Member> output = Context.Members
.SingleOrDefaultAsync(e => e.Id == id);
return await output != null
? new MemberVm(output)
: null;
}
由于new MemberVm(output)
,因此无法编译。相反,计算机要求我执行new MemberVm(await output)
。如果这是一个简单的return语句,我会理解,但是在这种情况下,在评估条件表达式时已经在等待它。在我看来,这就像是伪代码。
if(await output != null)
return await-again-but-why new MemberVm(output)
else
return null;
我做错了吗?或者仅仅是语言语法的意外和不幸的结果?
答案 0 :(得分:3)
如果您还没有阅读async..await
的工作原理,则可能应该更好地进行思考;但是这些关键字的主要作用是触发将原始代码自动改写为连续传递样式。
基本上会发生的是将原始代码转换为:
public Task<MemberVm> GetMember(Guid id)
{
Task<Member> output = Context.Members
.SingleOrDefaultAsync(e => e.Id == id);
return output.ContinueWith((Task<Member> awaitedOutput) =>
awaitedOutput.Result != null ? new MemberVm(output.Result) : null);
}
原始output
变量保持不变,等待的结果(可以这么说)在可用时传递到延续中。由于您没有将其保存到变量中,因此在首次使用后将无法使用。 (这是我称为awaitedOutput
的lambda参数,如果您自己不将等待的输出分配给变量,它实际上可能是C#编译器生成的乱码。)
在您的情况下,将等待的值存储在变量中可能是最简单的
public Task<MemberVm> GetMember(Guid id)
{
Member output = await Context.Members
.SingleOrDefaultAsync(e => e.Id == id);
return output != null
? new MemberVm(output)
: null;
}
您也可能直接在output.Result
下的代码中使用await
,但这并不是您应该做的事情,而且它容易出错。 (如果由于某种原因无意中将output
重新分配给其他任务。这将导致整个线程Wait()
,而我的猜测是冻结了。)
至关重要的是,说“已经等待的呼叫”没有任何意义。在后台,等待不是您要做呼叫或任务的事情;这是一条指令,指示编译器在等待之后获取所有代码,将其打包成一个闭包,将其传递给Task.ContinueWith()
,然后立即返回新任务。也就是说:await本身并不会导致对调用结果的等待,它会导致将等待代码注册为回调,只要结果可用就将被调用。如果您等待任务的结果已经可用,则所有更改就是将尽快调用此回调。
这种实现异步的方式是,控制在您需要等待调用完成的每个点返回到代码外部的某个事件循环。此事件循环监视“从外部”到达的东西(例如,某些I / O操作完成),并唤醒等待该过程的任何继续链。当您await
多次使用同一Task
时,所有发生的就是它处理多个此类回调。
(假设,是的,编译器也可以转换代码,以便在等待之后,原始变量名引用新值。但是有很多原因使我认为它不是以这种方式实现的-a的类型变量更改中间函数在C#中是前所未有的,并且会造成混淆;通常,总体看来,这将变得更加复杂且难以推理。)
在这里进行希望的说明性切线:我相信,当您两次使用同一await
函数的一个async
任务时,或多或少会发生以下情况:
ContinueWith()
,控制权返回顶层。ContinueWith()
如您所见,两次等待相同的Task在事件循环中几乎毫无意义。如果您需要任务的值,只需将其放入变量中即可。根据我的经验,很多时候您最好立即await
调用任何async
函数,并将此语句移到尽可能使用任务结果的位置。 (由于await之后的任何代码直到结果可用后才会运行,即使它没有使用该结果也是如此。)例外是如果您有一些代码需要在开始调用之后但在使用结果之前运行,您可能由于某种原因不能在通话开始前就运行。
答案 1 :(得分:3)
这里已经有正确的答案,但是解释却比必要的复杂得多。 await
关键字在任务完成和展开之前都保持执行状态(即Task<Member>
变成Member
)。但是,您不是坚持那个展开的部分。
第二个output
仍然是Task<Member>
。现在已经完成了,但是没有解开,因为您没有保存结果。
答案 2 :(得分:1)
这行吗?
MemberVm的构造函数是什么样的?
public async Task<MemberVm> GetMember(Guid id)
{
var output = await Context.Members
.SingleOrDefaultAsync(e => e.Id == id);
if (output == null)
return null;
return new MemberVm(output);
}
似乎MemberVm的构造函数没有在其构造函数中使用Task参数(尽管没有看到代码,但我不能确定)。相反,我认为构造函数只需要一个常规的MemberVm参数,因此通过评估Context.Members ...调用之前,应该可以帮助您解决所有问题。如果没有,请告诉我,我们会解决的。
答案 3 :(得分:1)
由于output
是Task
,而不是Member
,因此无法编译。
这将起作用:
public async Task<MemberVm> GetMember(Guid id)
{
Member member = await Context.Members
.SingleOrDefaultAsync(e => e.Id == id);
return member != null
? new MemberVm(member)
: null;
}
这不是:
Task<Member> output = Context.Members
.SingleOrDefaultAsync(e => e.Id == id);
return await output != null // <= "await output" is null or a Member instance
? new MemberVm(output) // "output" is always a Task<Member>
: null;
通过写await output
“输出”本身不会被等待结果代替。仍然与您在上面创建的任务相同。
不相关:我不建议返回null
。我想我可以MemberVM
处理null
的设置,或者如果这有力地表明应用程序代码或数据库一致性出问题,则抛出异常。
答案 4 :(得分:1)
理解编译器标记是类型问题是很重要的。 MemberVm
构造函数带有一个Member
参数,但是您的output
变量的类型为Task<Member>
。编译器实际上并不希望您再次等待Task,但这是从Task中提取结果并使类型工作的最常用方法。重写代码的另一种方法是更改output
的类型:
Member output = await Context.Members.SingleOrDefaultAsync(e => e.Id == id);
现在,您可以将output
直接传递给MemberVm
构造函数,因为您已经保存了第一次等待的结果。