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

时间:2014-07-29 14:46:44

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

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

  1. 马上抛出异常
  2. 返回将以异常
  3. 结束的任务

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

    示例:

    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解决方案吗?

2 个答案:

答案 0 :(得分:12)

大多数Task - 返回方法适用于async / await(因此不应在内部使用Task.RunTask.Factory.StartNew。< / p>

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

await CheckWebPageAsync();

差异只发生在调用方法然后等待的时候:

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

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

  

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

没有标准。前置条件是boneheaded exception的一种类型,所以抛出它并不重要,因为它永远不会被捕获

Jon Skeet认为应该直接抛出先决条件(&#34;外部&#34;返回的任务):

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运算符提供了一个很好的并行,它们可以保证抛出异常&#34;早期&#34;像这样(在调查员之外)。

但我不认为这是必要的。在任务中抛出前置条件时,我发现代码更简单:

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的一点。所以你的里程可能会有所不同......很多。 :)

答案 1 :(得分:0)

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

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

方法1:

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

方法2:

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
}

方法3:

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

方法4:

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 => {}))处理异常,则延续将根本无法运行。这类似于您问题中的示例。
  • 方法2实际上效果很好,但是您必须忍受警告或插入#pragma禁止内容。该方法可能最终异步运行,从而导致不必要的上下文切换。
  • 方法3似乎是最直观的。虽然有一个副作用-stacktrace根本不显示DoSomethingAsync()!您所看到的只是呼叫者。取决于您要抛出多少个相同类型的异常,这可能会很糟糕。
  • 方法4与方法2类似。您可以await + catch例外;您可以执行任务继续; stacktrace没有丢失的信息。它也同步运行,这对于非常轻量/快速的方法很有用。但是...对于您实​​现的每种方法,以这种方式编写它都非常尴尬。

请注意,我正在从实现的角度进行讨论-我无法控制其他人如何调用我的方法。目的是以一种正常执行的方式实现,而与调用方法无关。