繁体   English   中英

返回 Task 的方法应该抛出异常吗?

[英]Should methods that return Task throw exceptions?

返回Task的方法有两个报告错误的选项:

  1. 立即抛出异常
  2. 返回将以异常结束的任务

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

例子:

class PageChecker 
{
    Task CheckWebPage(string url) 
    {
        if(url == null) // Argument check
            throw Exception("Bad URL");
            
        // Some other synchronous check
        if(!HostPinger.IsHostOnline(url)) 
            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 是否有解决方案?

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

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

await CheckWebPageAsync();

差异仅在调用该方法然后稍后等待时出现:

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

但是,通常调用( CheckWebPagesAsync() )和await位于同一代码块中,因此无论如何它们都将位于同一try / catch块中,在这种情况下(通常)也无关紧要。

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

没有标准。 前置条件是一种愚蠢的异常,所以它是如何抛出的并不重要,因为它不应该被捕获

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");
}

请记住,永远不应该有任何代码捕获 preconditions ,因此在现实世界中,抛出异常的方式不应该有任何区别。

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

我有非常相似的问题/疑问。 我试图实现在接口中指定的异步方法(例如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;
    }
}

就像斯蒂芬克利里提到的那样,所有这些通常都有效。 但是,存在一些差异。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM