等待任务时忽略特定类型异常的更简单方法
A simpler way of ignoring specific types of exceptions when awaiting a Task
在等待 Task
时,我希望有一种简单的方法来忽略特定类型的异常,例如 OperationCanceledException
或 TimeoutException
或其他。我想到了编写一个可以包装我的 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 Task
s 的情况下,您必须 "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
之后成功的投回去了调用,由最后一个基于 TResult
的 Ignore
调用。上面的代码已经过修改以纠正这种情况。这并没有使整个模式特别优雅,但至少它现在看起来工作正常。
正如 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);
}
在等待 Task
时,我希望有一种简单的方法来忽略特定类型的异常,例如 OperationCanceledException
或 TimeoutException
或其他。我想到了编写一个可以包装我的 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 Task
s 的情况下,您必须 "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>()
.
编辑
基于 InvalidCastException
作为中间 Ignore 调用实际上 改变了 Task 并且原始 Task 在调用链中的某个地方丢失了。所以对"casting"Ignore
方法稍微做了适配,让return在最后完成原来的任务,这样就可以在Ignore
之后成功的投回去了调用,由最后一个基于 TResult
的 Ignore
调用。上面的代码已经过修改以纠正这种情况。这并没有使整个模式特别优雅,但至少它现在看起来工作正常。
正如 Vector Sigma 的 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);
}