【问题标题】:Should methods that return Task throw exceptions?返回 Task 的方法应该抛出异常吗?
【发布时间】:2020-11-03 17:12:51
【问题描述】:

返回Task的方法有两种报错选项:

  1. 立即抛出异常
  2. 返回将异常完成的任务

调用者应该期待这两种类型的错误报告,还是有一些标准/协议将任务行为限制为第二个选项?

例子:

class PageChecker {
    Task CheckWebPage(string url) {
        if(url == null) // Argument check
            throw Exception("Bad URL");

        if(!HostPinger.IsHostOnline(url)) // Some other synchronous check
            throw Exception("Host is down");

        return Task.Factory.StartNew(()=> {
            // Asynchronous check
            if(PageDownloader.GetPageContent(url).Contains("error"))
                throw Exception("Error on the page");
        });
    }
}

处理这两种类型看起来很丑:

try {
    var task = pageChecker.CheckWebPage(url);

    task.ContinueWith(t =>
        {
            if(t.Exception!=null)
                ReportBadPage(url);
        });

}
catch(Exception ex) {
    ReportBadPage(url);
}

使用 async/await 可能会有所帮助,但是对于没有异步支持的普通 .NET 4 是否有解决方案?

【问题讨论】:

标签: c# .net asynchronous .net-4.0 task-parallel-library


【解决方案1】:

大多数Task-returning 方法旨在与async/await 一起使用(因此不应在内部使用Task.RunTask.Factory.StartNew)。

注意,使用调用异步方法的常用方式,如何抛出异常并不重要:

await CheckWebPageAsync();

只有当方法被调用然后等待之后才会有区别:

List<Task> tasks = ...;
tasks.Add(CheckWebPagesAsync());
...
await Task.WhenAll(tasks);

但是,通常调用 (CheckWebPagesAsync()) 和 await 在同一个代码块中,所以它们无论如何都会在同一个 try/catch 块中,在这种情况下它也 (通常)没关系。

是否有一些标准/协议将任务行为限制为第二个选项?

没有标准。先决条件是boneheaded exception 的一种类型,因此如何抛出它并不重要,因为它永远不应该被捕获

Jon Skeet 认为应该直接抛出先决条件(“在返回的任务之外”):

Task CheckWebPageAsync(string url) {
  if(url == null) // argument check            
    throw Exception("Bad url");                     

  return CheckWebPageInternalAsync(url);
}

private async Task CheckWebPageInternalAsync(string url) {
  if((await PageDownloader.GetPageContentAsync(url)).Contains("error")) 
    throw Exception("Error on the page");
}

这为 LINQ 运算符提供了很好的并行性,可以保证像这样(在枚举器之外)“提前”抛出异常。

但我认为没有必要。我发现在任务中抛出前置条件时代码更简单:

async Task CheckWebPageAsync(string url) {
  if(url == null) // argument check            
    throw Exception("Bad url");                     

  if((await PageDownloader.GetPageContentAsync(url)).Contains("error")) 
    throw Exception("Error on the page");
}

请记住,绝不应该有任何代码可以捕获前置条件,因此在现实世界中,抛出异常的方式应该没有任何区别。

另一方面,这我实际上不同意 Jon Skeet 的一点。所以你的里程可能会有所不同......很多。 :)

【讨论】:

  • Stephen,虽然肯定不是问题或您的答案的核心,但在代码示例中指出应该抛出更具体的异常可能是有用的。例如:throw new ArgumentNullException("URL cannot be null.", "url")throw new ArgumentException("Error on the page.", "url")
【解决方案2】:

我遇到了非常相似的问题/疑问。我试图实现在接口中指定的异步方法(例如public Task DoSomethingAsync())。换句话说,接口期望特定函数 (DoSomething) 是异步的。

然而,事实证明该实现可以同步完成(而且我认为它的方法也不会花费很长时间来完成)。

public interface IFoobar
{
    Task DoSomethingAsync(Foo foo, Bar bar);
}

public class Caller
{
    public async void Test
    {
        try
        {
            await new Implementation().DoSomethingAsync(null, null);
        }
        catch (Exception e)
        {
            Logger.Error(e);
        }
    }
}

现在有四种方法可以做到这一点。

方法一:

public class Implementation : IFoobar
{
    public Task DoSomethingAsync(Foo foo, Bar bar)
    {
        if (foo == null)
            throw new ArgumentNullException(nameof(foo));
        if (bar == null)
            throw new ArgumentNullException(nameof(bar));

        DoSomethingWithFoobar(foo, bar);
    }
}

方法二:

public class Implementation : IFoobar
{
    #pragma warning disable 1998
    public async Task DoSomethingAsync(Foo foo, Bar bar)
    {
        if (foo == null)
            throw new ArgumentNullException(nameof(foo));
        if (bar == null)
            throw new ArgumentNullException(nameof(bar));

        DoSomethingWithFoobar(foo, bar);
    }
    #pragma warning restore 1998
}

方法三:

public class Implementation : IFoobar
{
    public Task DoSomethingAsync(Foo foo, Bar bar)
    {
        if (foo == null)
            return Task.FromException(new ArgumentNullException(nameof(foo)));
        if (bar == null)
            return Task.FromException(new ArgumentNullException(nameof(bar)));

        DoSomethingWithFoobar(foo, bar);
        return Task.CompletedTask;
    }
}

方法四:

public class Implementation : IFoobar
{
    public Task DoSomethingAsync(Foo foo, Bar bar)
    {
        try
        {
            if (foo == null)
                throw new ArgumentNullException(nameof(foo));
            if (bar == null)
                throw new ArgumentNullException(nameof(bar));
        }
        catch (Exception e)
        {
            return Task.FromException(e);
        }

        DoSomethingWithFoobar(foo, bar);
        return Task.CompletedTask;
    }
}

就像 Stephen Cleary 提到的那样,所有这些通常都有效。不过,还是有一些区别的。

  • 方法 1 要求您同步捕获异常(在调用方法时而不是在等待时)。如果您使用延续 (task.ContinueWith(task =&gt; {})) 来处理异常,延续将根本不会运行。这与您问题中的示例类似。
  • 方法 2 实际上效果很好,但您将不得不接受警告或插入#pragma 抑制。该方法最终可能会异步运行,从而导致不必要的上下文切换。
  • 方法 3 似乎是最直观的。不过有一个副作用——stacktrace 根本不显示DoSomethingAsync()!你所能看到的只是来电者。这可能会很糟糕,具体取决于您抛出了多少相同类型的异常。
  • 方法4和方法2类似,可以await+catch例外;你可以做任务延续;堆栈跟踪没有丢失的信息。它也是同步运行的,这对于非常轻量/快速的方法很有用。但是......对于你实现的每个方法都这样写是非常尴尬的。

请注意,我是从实现的角度讨论这个问题 - 我无法控制其他人如何调用我的方法。目的是以正常执行的方式实现,独立于调用方法。

【讨论】:

    猜你喜欢
    • 2011-10-13
    • 2017-12-11
    • 2013-05-20
    • 1970-01-01
    • 2012-12-28
    • 2011-04-27
    • 2012-07-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多