Повторное удаление предыдущего исключения внутри ContinueWith
Введение
После некоторого недоумения над моим кодом я обнаружил, что исключения не обязательно распространяются через ContinueWith
:
int zeroOrOne = 1;
Task.Factory.StartNew(() => 3 / zeroOrOne)
.ContinueWith(t => t.Result * 2)
.ContinueWith(t => Console.WriteLine(t.Result))
.ContinueWith(_ => SetBusy(false))
.LogExceptions();
В этом примере строка SetBusy
"сбрасывает" цепочку исключений, поэтому исключение "деление на ноль" не видно и впоследствии взрывается на моем лице "Исключение (исключения) задачи).."
Итак... Я написал себе небольшой метод расширения (с множеством разных перегрузок, но в основном все это делает):
public static Task ContinueWithEx(this Task task, Action<Task> continuation)
{
return task.ContinueWIth(t =>
{
if(t.IsFaulted) throw t.Exception;
continuation(t);
});
}
Поиск еще немного, я наткнулся на это сообщение в блоге, где он предлагает аналогичное решение, но использует TaskCompletionSource, который (перефразированный ) выглядит следующим образом:
public static Task ContinueWithEx(this Task task, Action<Task> continuation)
{
var tcs = new TaskCompletionSource<object>();
return task.ContinueWith(t =>
{
if(t.IsFaulted) tcs.TrySetException(t.Exception);
continuation(t);
tcs.TrySetResult(default(object));
});
return tcs.Task;
}
Вопрос
Являются ли эти две версии строго эквивалентными? Или есть тонкая разница между throw t.Exception
и tcs.TrySetException(t.Exception)
?
Кроме того, делает ли тот факт, что, по-видимому, только один человек во всем Интернете, который сделал это, указывает, что мне не хватает идиоматического способа сделать это?
Ответы
Ответ 1
Разница между ними тонкая. В первом примере вы выбрасываете исключение, возвращаемое из задачи. Это вызовет обычное бросание и ловушку исключения в CLR, ContinueWith
будет захватывать и обертывать его и передавать его следующей задаче в цепочке.
Во втором вы вызываете TrySetException
, который все равно будет обертывать исключение и передавать его следующей задаче в цепочке, но не вызывает никакой логики try/catch.
Конечный результат после одного ContinueWithEx
равен AggregateException(AggregateException(DivideByZeroException))
. Единственное различие, которое я вижу, заключается в том, что внутреннее AggregateException имеет трассировку стека, установленную в первом примере (поскольку она была выбрана), а во втором примере нет трассировки стека.
Ни один из них не может быть значительно быстрее, чем другой, но я бы предпочел, чтобы второй избежал ненужных бросков.
Я сделал что-то подобное, когда продолжение вернуло результат. Я назвал его Select
, обработал случаи отмены предыдущей задачи, предоставил перегрузки для изменения исключения вместо или в дополнение к результату и использовал параметр ExecuteSynchronously
. Когда продолжение само по себе вернет задачу, я назвал это Then
вместо этого на основе кода из этой статьи