Должны ли методы, возвращающие исключения задачи, исключать?

У методов, возвращающих 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 -returning предназначены для использования с 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");
}

Помните, что никогда не будет никакого кода, который ловит предварительные условия, поэтому в реальном мире не должно различаться, как генерируется исключение.

С другой стороны, это один момент, когда я фактически не согласен с Джоном Скитом. Таким образом, ваш пробег может меняться... много.:)