等待任务时忽略特定类型异常的更简单方法

A simpler way of ignoring specific types of exceptions when awaiting a Task

在等待 Task 时,我希望有一种简单的方法来忽略特定类型的异常,例如 OperationCanceledExceptionTimeoutException 或其他。我想到了编写一个可以包装我的 Task 并抑制 Exception 类型的扩展方法,我将把它作为参数。所以我写了这个:

public static async Task Ignore<TException>(this Task task)
    where TException : Exception
{
    try
    {
        await task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return;
        throw;
    }
}

我可以像这样使用它并且它工作正常:

await myTask.Ignore<OperationCanceledException>();

问题是它只支持一种类型的异常,我必须为两种类型编写另一个版本,为三种类型编写另一个版本,为四种类型编写另一个版本,等等。这已经失控了,因为我还想要这个扩展的重载,它将忽略 return 结果 (Task<Result>) 的任务异常。所以我还需要另外一系列的扩展方法来涵盖这种情况。

这是我在等待 Task<Result> 时忽略一种异常的实现:

public static async Task<TResult> Ignore<TResult, TException>(
    this Task<TResult> task, TResult defaultValue)
    where TException : Exception
{
    try
    {
        return await task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return defaultValue;
        throw;
    }
}

用法:

var result = await myInt32Task.Ignore<int, OperationCanceledException>(0);

我的问题是,我能否以一种可以处理多种类型的被忽略异常的方式编写这些方法,而不必为每种类型编写一个单独的方法?

是的,你可以,但你不能使用 Generics

如果您愿意将 Type 作为 params 传递,您可以这样做:

public static async Task<TResult> Ignore<TResult>
        (this Task<TResult> task, TResult defaultValue, params Type[] typesToIgnore)
        {
            try
            {
                return await task;
            }
            catch (Exception ex)
            {
                if (typesToIgnore.Any(type => type.IsAssignableFrom(ex.GetType())))
                {
                    return defaultValue;
                }

                throw;
            }
        }

现在,这没那么吸引人了 并且 你没有 generic constraint (where TException...) 但它应该得到这份工作完成。

我会依靠任务链来避免在扩展方法中初始化任务执行。

public static Task<TResult> Ignore<TResult>(this Task<TResult> self, TResult defaultValue, params Type[] typesToIgnore)
{
    return self.ContinueWith(
        task =>
        {
            if (task.IsCanceled 
                && (typesToIgnore.Any(t => typeof(OperationCanceledException) == t || t.IsSubclassOf(typeof(OperationCanceledException))))) {
                return defaultValue;
            }

            if (!task.IsFaulted)
            {
                return task.Result;
            }

            if (typesToIgnore.Any(t => task.Exception.InnerException.GetType() == t ||
                                task.Exception.InnerException.GetType().IsSubclassOf(t)))
            {
                return defaultValue;
            }

            throw task.Exception.InnerException;
        }, TaskContinuationOptions.ExecuteSynchronously);
}

据我了解,您希望在等待 Task 时能够忽略一种以上的异常。您自己的解决方案对我来说似乎是您的最佳选择。您总是可以简单地 "chain" 使用您提出的解决方案进行调用:

await myTask.Ignore<OperationCanceledException>().Ignore<IOException>().Ignore<TimeoutException>();

这应该 return 一个任务,本质上是三个嵌套的 try-catch 块。除非你主动想要一个更优雅的定义,否则你总是可以使用更优雅的用法;)

唯一不太优雅的问题是,在 TResult-returning Tasks 的情况下,您必须 "propagate" 默认值值多次。如果这不是一个非常大的问题,那么你可以逃避同样的问题:

await myTask.Ignore<int, OperationCanceledException>(0).Ignore<int, TimeoutException>(0);

作为 "obscure" 的好处,请注意,通过这种方式,您可以非常轻松地为 不同的异常提供 不同的默认 return 值。所以必须重复默认值可能最终会变成你的优势!例如,您可能有理由在 TimeOutException 上 return 0 但在 OperationCanceledException 上为 -1 等。如果这最终成为您的 目的 ,请记住使用 is 可能不是你真正想要的,而不是精确的 Type 相等,因为你可能还想 return 不同的默认值用于派生自相同 Type 的不同异常(此分析是开始变得相当复杂,但你当然明白了。

更新

基于 TResult 的版本的链式调用 "elegance" 的最终水平似乎必须以编译时类型检查为代价:

public static async Task<TResult> Ignore<TResult, TException>(
this Task<TResult> task, TResult defaultValue)
    where TException : Exception
{
    try
    {
        return await task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return defaultValue;
        throw;
    }
}

public static async Task<TResult> Ignore<TResult, TException>(
this Task task, TResult defaultValue)
    where TException : Exception
{
    try
    {
        return await (Task<TResult>)task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return defaultValue;
        throw;
    }
}

public static Task Ignore<TException>(this Task task)
    where TException : Exception
{
    try
    {
        //await seems to create a new Task that is NOT the original task variable.
        //Therefore, trying to cast it later will fail because this is not a Task<TResult>
        //anymore (the Task<TResult> has been "swallowed").

        //For that reason, await the task in an independent function.
        Func<Task> awaitableCallback = async () => await task;

        awaitableCallback();

        //And return the original Task, so that it can be cast back correctly if necessary.
        return task;
    }
    catch (Exception ex)
    {
        //Same upon failure, return the original task.
        if (ex is TException) return task;
        throw;
    }
}

public static async Task<int> TestUse()
{
    Task<int> t = Task<int>.Run(() => 111);

    int result = await t.Ignore<TaskCanceledException>()
                        .Ignore<InvalidOperationException>()
                        .Ignore<int, TimeoutException>(0);

    return result;
}

如果您准备牺牲编译时安全性,您可以通过仅声明您希望忽略的异常并在最后添加 "casting" 调用来减轻重复的痛苦。当然,这有其自身的问题,但只有在需要忽略多个异常时才需要这样做。否则,您可以使用单一类型并相应地调用 Ignore<TResult, TException>().

编辑

基于 ,因为 async/await 模式似乎生成了一个新任务,该任务包装了上面忽略方法中传递的可等待任务参数,示例调用确实失败并显示 InvalidCastException 作为中间 Ignore 调用实际上 改变了 Task 并且原始 Task 在调用链中的某个地方丢失了。所以对"casting"Ignore方法稍微做了适配,让return在最后完成原来的任务,这样就可以在Ignore之后成功的投回去了调用,由最后一个基于 TResultIgnore 调用。上面的代码已经过修改以纠正这种情况。这并没有使整个模式特别优雅,但至少它现在看起来工作正常。

正如 Vector Sigma 的 , it is possible to chain my original one-type methods to achieve ignoring multiple types of exceptions. Chaining the Ignore for Task<TResult> is quite awkward though, because of the required repetition of the TResult type and the defaultValue. After reading the accepted answer of a question about partial type inference 中指出的那样,我找到了解决此问题的方法。我需要引入一个通用的任务包装器 struct 来保持这个状态,并包含一个可链接的方法 Ignore。这是预期用途:

var result = await myInt32Task.WithDefaultValue(0)
    .Ignore<OperationCanceledException>()
    .Ignore<TimeoutException>();

这是我命名为 TaskWithDefaultValue 的任务包装器和扩展方法 WithDefaultValue

public readonly struct TaskWithDefaultValue<TResult>
{
    private readonly Task<TResult> _task;
    private readonly TResult _defaultValue;

    public TaskWithDefaultValue(Task<TResult> task, TResult defaultValue)
    {
        _task = task;
        _defaultValue = defaultValue;
    }

    public Task<TResult> GetTask() => _task;
    public TaskAwaiter<TResult> GetAwaiter() => _task.GetAwaiter();

    public TaskWithDefaultValue<TResult> Ignore<TException>()
        where TException : Exception
    {
        var continuation = GetContinuation(_task, _defaultValue);
        return new TaskWithDefaultValue<TResult>(continuation, _defaultValue);

        async Task<TResult> GetContinuation(Task<TResult> t, TResult dv)
        {
            try
            {
                return await t.ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                if (ex is TException) return dv;
                throw;
            }
        }
    }
}

public static TaskWithDefaultValue<TResult> WithDefaultValue<TResult>(
    this Task<TResult> task, TResult defaultValue)
{
    return new TaskWithDefaultValue<TResult>(task, defaultValue);
}