Должны ли методы, возвращающие Task, генерировать исключения?
Методы, которые возвращают Task
есть два варианта сообщения об ошибке:
- бросить исключение сразу
- возврат задания, которое закончится с исключением
Должен ли звонящий ожидать оба типа сообщений об ошибках или есть какой-то стандарт / соглашение, которое ограничивает поведение задачи вторым вариантом?
Пример:
class PageChecker {
Task CheckWebPage(string url) {
if(url == null) // Argument check
throw Exception("Bad URL");
if(!HostPinger.IsHostOnline(url)) // Some other synchronous check
throw Exception("Host is down");
return Task.Factory.StartNew(()=> {
// Asynchronous check
if(PageDownloader.GetPageContent(url).Contains("error"))
throw Exception("Error on the page");
});
}
}
Работа с обоими типами выглядит довольно некрасиво:
try {
var task = pageChecker.CheckWebPage(url);
task.ContinueWith(t =>
{
if(t.Exception!=null)
ReportBadPage(url);
});
}
catch(Exception ex) {
ReportBadPage(url);
}
Использование async / await может помочь, но есть ли решение для простого.NET 4 без асинхронной поддержки?
1 ответ
Наиболее Task
методы возврата предназначены для использования с async
/await
(и как таковой не следует использовать Task.Run
или же Task.Factory.StartNew
внутри).
Обратите внимание, что при обычном способе вызова асинхронных методов не имеет значения, как генерируется исключение:
await CheckWebPageAsync();
Разница возникает только тогда, когда метод вызывается, а затем ожидается позже:
List<Task> tasks = ...;
tasks.Add(CheckWebPagesAsync());
...
await Task.WhenAll(tasks);
Однако обычно звонок (CheckWebPagesAsync()
) и await
находятся в одном блоке кода, поэтому они будут в том же try
/catch
блокировать в любом случае, и в этом случае это также (обычно) не имеет значения.
Есть ли какой-то стандарт / соглашение, ограничивающее поведение задачи вторым вариантом?
Там нет стандарта. Предварительные условия являются типом исключения с тупой головой, поэтому на самом деле не имеет значения, как оно генерируется, потому что оно никогда не должно быть поймано.
Джон Скит считает, что предварительные условия должны быть выброшены напрямую ("вне" возвращаемого задания):
Task CheckWebPageAsync(string url) {
if(url == null) // argument check
throw Exception("Bad url");
return CheckWebPageInternalAsync(url);
}
private async Task CheckWebPageInternalAsync(string url) {
if((await PageDownloader.GetPageContentAsync(url)).Contains("error"))
throw Exception("Error on the page");
}
Это обеспечивает хорошую параллель операторам LINQ, которые гарантированно генерируют исключения "рано", как это (за пределами перечислителя).
Но я не думаю, что это необходимо. Я нахожу, что код проще, если бросить предварительные условия в задачу:
async Task CheckWebPageAsync(string url) {
if(url == null) // argument check
throw Exception("Bad url");
if((await PageDownloader.GetPageContentAsync(url)).Contains("error"))
throw Exception("Error on the page");
}
Помните, что никогда не должнобыть никакого кода, который ловит предварительные условия, поэтому в реальном мире не должно иметь никакого значения, как генерируется исключение.
С другой стороны, это тот момент, когда я действительно не согласен с Джоном Скитом. Так что ваш пробег может варьироваться... много.:)
У меня была очень похожая проблема / сомнения. Я пытался реализовать асинхронные методы (например,
public Task DoSomethingAsync()
), который был указан в интерфейсе. Перефразируя, интерфейс ожидает конкретную функцию (DoSomething
) быть асинхронным.
Однако оказывается, что реализация может делать это синхронно (и я не думаю, что этот метод также займет много времени).
public interface IFoobar
{
Task DoSomethingAsync(Foo foo, Bar bar);
}
public class Caller
{
public async void Test
{
try
{
await new Implementation().DoSomethingAsync(null, null);
}
catch (Exception e)
{
Logger.Error(e);
}
}
}
Теперь есть четыре способа сделать это.
Способ 1:
public class Implementation : IFoobar
{
public Task DoSomethingAsync(Foo foo, Bar bar)
{
if (foo == null)
throw new ArgumentNullException(nameof(foo));
if (bar == null)
throw new ArgumentNullException(nameof(bar));
DoSomethingWithFoobar(foo, bar);
}
}
Способ 2:
public class Implementation : IFoobar
{
#pragma warning disable 1998
public async Task DoSomethingAsync(Foo foo, Bar bar)
{
if (foo == null)
throw new ArgumentNullException(nameof(foo));
if (bar == null)
throw new ArgumentNullException(nameof(bar));
DoSomethingWithFoobar(foo, bar);
}
#pragma warning restore 1998
}
Способ 3:
public class Implementation : IFoobar
{
public Task DoSomethingAsync(Foo foo, Bar bar)
{
if (foo == null)
return Task.FromException(new ArgumentNullException(nameof(foo)));
if (bar == null)
return Task.FromException(new ArgumentNullException(nameof(bar)));
DoSomethingWithFoobar(foo, bar);
return Task.CompletedTask;
}
}
Метод 4:
public class Implementation : IFoobar
{
public Task DoSomethingAsync(Foo foo, Bar bar)
{
try
{
if (foo == null)
throw new ArgumentNullException(nameof(foo));
if (bar == null)
throw new ArgumentNullException(nameof(bar));
}
catch (Exception e)
{
return Task.FromException(e);
}
DoSomethingWithFoobar(foo, bar);
return Task.CompletedTask;
}
}
Как и то, что упомянул Стивен Клири, все это в целом работает. Однако есть некоторые отличия.
- Метод 1 требует, чтобы вы перехватили исключение синхронно (при вызове метода, а не при ожидании). Если вы использовали продолжение (
task.ContinueWith(task => {})
) для обработки исключения продолжение просто не будет запущено. Это похоже на пример в вашем вопросе. - Метод 2 на самом деле работает очень хорошо, но вам придется смириться с предупреждением или вставить
#pragma
подавления. В конечном итоге метод может работать асинхронно, вызывая ненужные переключения контекста. - Метод 3 кажется наиболее интуитивным. Однако есть один побочный эффект - stacktrace не показывает
DoSomethingAsync()
вообще! Все, что вы могли видеть, - это звонящего. Это может быть довольно плохо в зависимости от того, сколько исключений одного и того же типа вы выбрасываете. - Метод 4 аналогичен методу 2. Вы можете
await
+catch
исключение; вы могли делать продолжения задач; в трассировке стека нет недостающей информации. Он также работает синхронно, что может быть полезно для очень легких / быстрых методов. Но... ужасно неудобно писать так для каждого метода, который вы реализуете.
Обратите внимание, что я обсуждаю это с точки зрения реализации - я не могу контролировать, как кто-то другой может вызвать мой метод. Цель состоит в том, чтобы реализовать способ, который выполняется нормально, независимо от метода вызова.