问题
When awaiting a Task I would like to have an easy way to ignore specific types of exceptions, like OperationCanceledException
or TimeoutException
or whatever. I got the idea of writing an extension method that could wrap my Task
, and suppress the Exception
type I would give it as argument. So I wrote this one:
public static async Task Ignore<TException>(this Task task)
where TException : Exception
{
try
{
await task;
}
catch (Exception ex)
{
if (ex is TException) return;
throw;
}
}
I can use it like this and it works OK:
await myTask.Ignore<OperationCanceledException>();
The problem is that it supports only one type of exception, and I must write another version for two types, another one for three types, another one for four, and so on. This is getting out of hand, because I also want an overload of this extension that will ignore exceptions of tasks that return a result (Task<Result>
). So I will need another series of extension methods to cover this case as well.
Here is my implementation for ignoring one type of exception when awaiting a 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;
}
}
Usage:
var result = await myInt32Task.Ignore<int, OperationCanceledException>(0);
My question is, can I write these methods in a way that can handle multiple types of ignored exceptions, without having to write a separate method for each number of types?
回答1:
Yes you can, but you cannot do it using Generics
.
If you're willing to pass Type
s as params
, you can do this:
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;
}
}
Now, this is much less attractive and you don't have the generic constraint
(where TException
...) but it should get the job done.
回答2:
I would rely on task chaining to avoid initializing task execution in the extension method.
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);
}
回答3:
From what I understand, your desire is to be able to ignore more than 1 type of exceptions while awaiting your Task
. Your own solution appears to be your best option for me. You could always simply "chain" the calls using your proposed solution:
await myTask.Ignore<OperationCanceledException>().Ignore<IOException>().Ignore<TimeoutException>();
This should return a Task that is, in essence, three nested try-catch blocks. Unless you actively want a more elegant definition, you can always get away with more elegant usage ;)
The only not-so-elegant problem is that, in the case of your TResult
-returning Task
s, you have to "propagate" the default value multiple times. If this is not a very big problem, then you could get away with the same:
await myTask.Ignore<int, OperationCanceledException>(0).Ignore<int, TimeoutException>(0);
As an "obscure" bonus, note that this way you can very easily supply different default return values for different exceptions. So having to repeat the default value might turn to your advantage after all! For example you might have a reason to return 0 on TimeOutException but -1 on OperationCanceledException, etc. If this becomes your purpose in the end, remember that using is
might not be what you really want, rather than exact Type
equality, because you might want to also return different default values for different exceptions that derive from the same Type
(this analysis is starting to get rather complicated but you get the point, of course).
Update
The ultimate level of chained-call "elegance" for the TResult
-based version seems to have to come at the expense of compile-time type checking:
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;
}
If you are prepared to sacrifice compile-time safety, you can ease the pain of repetition by only stating the exceptions you wish to ignore and adding the "casting" call in the end. This has its own share of problems, of course, but you only need to do it when you need to ignore multiple exceptions. Otherwise, you are good with a single type and the corresponding single call to Ignore<TResult, TException>()
.
Edit
Based on the relevant comment, because the async/await pattern appears to spawn a new Task that wraps the awaitable task parameter passed in the Ignore methods above, the example call indeed fails with an InvalidCastException
as the intermediate Ignore calls in fact changed the Task and the original Task gets lost somewhere in the chain of calls. Therefore, the "casting" Ignore
method has been re-adapted slightly to enable returning the original task at the end, so that it can successfully be cast back after all Ignore
calls, by the last TResult
-based Ignore
call. The code above has been amended to correct this scenario. This does not make the entire pattern particularly elegant but at least it seems to be working properly now.
回答4:
As pointed out in Vector Sigma's answer, 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, I figured out how to fix this. I need to introduce a generic task-wrapper struct
that will hold this state, and contain a chainable method Ignore
. This is the intended usage:
var result = await myInt32Task.WithDefaultValue(0)
.Ignore<OperationCanceledException>()
.Ignore<TimeoutException>();
Here is the task-wrapper that I named TaskWithDefaultValue
, and the extension method 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);
}
来源:https://stackoverflow.com/questions/58465999/a-simpler-way-of-ignoring-specific-types-of-exceptions-when-awaiting-a-task