未处理的异步异常。

4

除了避免使用async void的原因之外,是否有可能从调用代码中捕获以下错误?

private static Action ErrorAction
{
  get
  {
    return async () =>
    {
      await Task.Delay(0);
      throw new NotImplementedException();
    };
  }
}

例如在以下两个示例中,我想收集抛出的异常,但是两个测试都失败了:
[Test]
public void SelfContainedExampleTryCatch()
{
  List<Exception> errors = new List<Exception>();
  try
  {
     ErrorAction();
  }
  catch (Exception ex)
  {
    errors.Add(ex);
  }
  errors.Count().Should().Be(1);
}

[Test]
public void SelfContainedExampleContinueWith()
{
  List<Exception> errors = new List<Exception>();
  var task = Task.Factory.StartNew(ErrorAction);
  task.ContinueWith(t =>
                {
                    errors.Add(t.Exception);
                }, TaskContinuationOptions.OnlyOnFaulted);
  task.Wait();

  errors.Count().Should().Be(1);
}

我知道我可以使用带有"async Task"标识的方法; 但具体来说,我需要处理可分配委托异步代码示例的Action。在全局级别捕获不可行。如果可以的话,对于同步和异步操作都通用的解决方案是理想的。希望有一个简单的解决方案,但到目前为止,我只是走了许多死路,并且(也许不正确地)得出结论它是不可能的。任何帮助(或节省时间)都将不胜感激!

在哪个点上测试失败了? - mason
你的意思是测试在ErrorAction();这一行出现未处理的异常导致失败了? - mason
1
@mason 在断言errors.Count().Should().Be(1);处的测试将会失败,因为异常不会被传播。 - Ilian
1
我发现如果我在内部处理程序中引入异步代码,我必须使调用代码异步,并且调用那个异步的代码也要异步。这最终会导致一直异步下去。你能解释一下为什么不能使用带有“async Task”签名的方法吗? - Andrew Shepherd
1
@AndrewShepherd 不必一路使用异步。您可以通过在未标记为异步的方法中运行异步命令 var task = MyCallAsync(); task.Wait(); 来执行它。 - mason
显示剩余6条评论
3个回答

2

有些情况下,框架要求您使用async void,而有时您可能真的不想这样做(例如ICommand.Execute)。在这些情况下,我通常建议引入一个await兼容的API,并将您的async void方法简单地包装起来:

static Func<Task> ErrorActionAsync
{
  get
  {
    return async () =>
    {
      await Task.Yield();
      throw new NotImplementedException();
    };
  }
}

private static Action ErrorAction
{
  get
  {
    return async () => { await ErrorActionAsync(); }
  }
}

顺便提一下,我将 await Task.Delay(0) 更改为 await Task.Yield(),因为 await Task.Delay(0) 是一个空操作。

然后您可以在单元测试中使用 ErrorActionAsync 而不是 ErrorAction。这种方法确实意味着您会有少量未经测试的代码(async void 包装器 - ErrorAction)。

但是,如果您想保持 ErrorAction 不变,并且它可以是同步的或异步的,则还有另一种可能性。我编写了一个名为 AsyncContext 类型,您可以像这样使用:

[Test]
public void SelfContainedExampleTryCatch()
{
  List<Exception> errors = new List<Exception>();
  try
  {
    AsyncContext.Run(() => ErrorAction());
  }
  catch (Exception ex)
  {
    errors.Add(ex);
  }
  errors.Count().Should().Be(1);
}

AsyncContext.Run将会阻塞,直到async void方法完成,并且会捕获async void方法中的异常并直接抛出。


0

如果您希望ErrorAction能够处理异步调用并且仍然能够在调用站点处理异常,那么我认为您只需要咬紧牙关并将ErrorAction更改为返回Task

如果您决定更改合同,则可能最好保持现有非异步实现的同步性质。您可以通过以下方式实现:

private static Task ErrorAction
{
  get
  {
    // Synchronous code here

    return Task.FromResult(true);
  }
}

-2

我认为可以在不改变合同的情况下返回任务。看这个例子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;

namespace ConsoleApplication1
    {
    class Program
        {
        static void Main(string[] args)
            {
            SelfContainedExampleTryCatch();
            SelfContainedExampleContinueWith();
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
            }

        public static void SelfContainedExampleTryCatch()
            {
            var errors = new List<Exception>();
            try
                {
                ErrorAction();
                }
            catch (Exception ex)
                {
                errors.Add(ex);
                }
            errors.Count().Should().Be(1);
            }

        public static void SelfContainedExampleContinueWith()
            {
            var errors = new List<Exception>();
            var task = Task.Factory.StartNew(ErrorAction);
            task.ContinueWith(t =>
            {
                errors.Add(t.Exception);
            }, TaskContinuationOptions.OnlyOnFaulted);
            task.Wait();

            errors.Count().Should().Be(1);
            }

        private static Action ErrorAction
            {
            get
                {
                return () => DoTask().Wait();
                }
            }

        private static async Task DoTask() {
            await Task.Delay(0);
            throw new NotImplementedException();
            }
        }
    }

ContinueWith测试通过了,但是另一个测试没有通过。我相信这是你的意图。


1
@lonewolf 我相信这是无法避免的。如果你不想被阻止,那么你需要改变合同。 - mason
问题在于我有一个公开的合同,它接受一个Action - 我无法控制第三方传递什么,因此重新排列输入除非我可以在接受的操作和调用代码之间放置一些东西,否则不会起作用,尽管感谢您的建议。 - 9swampy

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