VoidTaskResult类型在异步方法中是什么意思?

15

我最近第一次使用async(和.Net 4.5),遇到了一些困惑的问题。我在网上找不到关于 VoidTaskResult 类的太多信息,所以我来这里看看是否有人对发生了什么有什么想法。

我的代码类似于以下内容。显然,这简化了很多。基本思路是调用插件方法,这些方法是异步的。如果它们返回Task,那么异步调用就没有返回值。如果它们返回Task<>,则会有返回值。我们事先不知道它们的类型,所以想法是使用反射查看结果的类型(如果类型为Type<>,则IsGenericType为真)并使用动态类型获取值。

在我的实际代码中,我通过反射调用插件方法。我认为这不应该影响我看到的行为。

// plugin method
public Task yada()
{
 // stuff
}

public async void doYada()
{
  Task task = yada();
  await task;

  if (task.GetType().IsGenericType)
  {
    dynamic dynTask = task;
    object result = dynTask.Result;
    // do something with result
  }
}

对于上面展示的插件方法,这很有效。 IsGenericType为false(如预期所述)。

然而,如果稍微更改插件方法的声明,IsGenericType现在返回true,会导致出现问题:

public async Task yada()
{
 // stuff
}
当您执行此操作时,object result = dynTask.Result;这一行会抛出以下异常:

RuntimeBinderException

如果深入研究任务对象,它实际上似乎是Type<VoidTaskResult>VoidTaskResult是 Threading 命名空间中的一个私有类型,几乎没有任何内容。

VoidTaskResult task

我尝试更改我的调用代码:
public async void doYada()
{
  Task task = yada();
  await task;

  if (task.GetType().IsGenericType)
  {
    object result = task.GetType().GetProperty("Result").GetMethod.Invoke(task, new object[] { });
    // do something with result
  }
}

这种情况下,“成功”指的是不再抛出异常,但现在结果是 VoidTaskResult 类型,我无法合理地处理它。

我应该补充说明的是,我甚至很难为所有这些问题制定一个真正的问题。也许我的真正问题是像“VoidTaskResult是什么?”或者“当我动态调用异步方法时为什么会发生这种奇怪的事情?”甚至可能是“如何调用可选异步的插件方法?” 无论如何,我将这个问题公开希望其中一位专家能够解答。

1个回答

20
这是由于任务类层次结构(特别是任务完成源)的设计方式导致的。
首先,Task<T>派生自Task。我假设你已经熟悉了这一点。
此外,您可以为执行代码的任务创建Task或Task<T>类型。例如,如果您的第一个示例返回Task.Run或其他内容,则会返回实际的Task对象。
问题出在考虑TaskCompletionSource<T>与任务层次结构的交互方式时。TaskCompletionSource<T>用于创建不执行代码而仅充当某些操作完成通知的任务。例如,超时、I/O包装器或async方法。
没有非泛型TaskCompletionSource类型,因此,如果您想要像这样的无返回值通知(例如,超时或async Task方法),则必须为某个T创建TaskCompletionSource<T>并返回Task<T>。async团队必须为async Task方法选择T,因此他们创建了VoidTaskResult类型。
通常这不是问题。由于Task<T>派生自Task,因此该值转换为Task并且每个人都很高兴(在静态世界中)。但是,由TaskCompletionSource<T>创建的每个任务实际上都是Task<T>类型,而不是Task类型,并且您可以使用反射/动态代码查看到这一点。
最终结果是,您必须像Task一样处理Task<VoidTaskResult>。但是,VoidTaskResult是实现细节;它将来可能会改变。
因此,我建议您基于yada的(声明的)返回类型而不是(实际的)返回值来构建逻辑。这更接近编译器的行为。
Task task = (Task)yadaMethod.Invoke(...);
await task;

if (yadaMethod.ReturnType.IsGenericType)
{
  ...
}

异步团队为什么要引入VoidTaskResult,而不是直接引入非泛型的TaskCompletionSource? - Puppy
@Puppy:我怀疑是因为这样做工作量少得多。编写一个非泛型的TaskCompletionSource并不太难,但任何公共API在发布之前都必须通过一系列其他“门槛”。安全审查等等。 - Stephen Cleary
1
解释很合理。但从实际角度来看,你如何在运行时确定一个没有结果的“Task”和一个有结果的“Task”之间的区别呢?也就是说,“Task<VoidTaskResult>”即使用于没有结果的任务,而且由于“VoidTaskResult”不是公共的,似乎唯一的检查方式就是比较“Task<T>”类型参数名称。我认为这在技术上没问题,但似乎不像C#在代码中必须指定类型名称作为字符串的方式那样优秀。 - Peter Duniho
@PeterDuniho:这种运行时代码肯定不像C#。因为编译器永远不需要担心它;它总是使用声明的类型。我很想听听你的情况;可能有更好的解决方案。 - Stephen Cleary
不是我的情况。我在阅读了这个问题之后遇到了这个问题。我提供了一个答案,我认为它解决了问题中的具体请求,但是依赖于非公共类型的_text_名称让我感到困扰(如果我可以使用typeof运算符就已经够糟糕了,但是必须将名称作为文本进行比较对我来说尤其令人恼火)。 - Peter Duniho

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