Создавать и использовать async void методы необходимо с большой осторожностью. Все дело в том, что они способны вызвать непредсказуемое поведение приложения. Поэтому использовать их можно только в случае, если разработчик твердо уверен в том, что он делает.
Давайте посмотрим на причины сделанного выше утверждения.
Невозможно использовать await (т.е. ожидать результат)
Предположим в классе есть следующий метод, загружающий текст страницы с сайта в поле _content:
private string _content;
private async void GetPageAsync(string url)
{
var client = new HttpClient();
this._content = await client.GetStringAsync(url);
}
На первый взгляд может показаться что все в порядке. Но использовать GetPageAsync() совместно с await не получится. Поэтому вызывающий код будет примерно следующего вида:
public void PrintLength()
{
this.GetPageAsync("http://ya.ru");
Console.WriteLine("Content length: {0}", this._content.Length);
}
В этом случае, как только выполнение дойдет до await client.GetStringAsync(), в основном потоке тут же продолжится выполнение PrintLength(). Соответственно, Console.WriteLine() выкинет исключение, т.к. в этот момент client.GetStringAsync() еще не загрузит страницу и _content будет равен null.
Возможен другой вариант. Если _content уже было инициализировано каким-либо не пустым значением, то метод просто выведет некорректный результат, выдавая его за правильный.
И, наконец, рассмотрим случай, если между вызовом GetPageAsync() и обращением к полю _content будет совершаться какая-то работа. Для примера имитируем её вызовом Thread.Sleep():
public void PrintLenght()
{
this.GetPageAsync("http://ya.ru");
Thread.Sleep(new Random().Next(5) * 1000); // "спим" от 0 до 5 секунд
Console.WriteLine("Content length: {0}", this._content.Length);
}
Теперь поведение кода будет полностью зависеть от продолжительности "сна". Если она меньше, чем время загрузки страницы, то получим исключение или старое значение _content, если же больше – WriteLine() отобразит на консоли корректный результат.
Невозможно определить когда выполнение завершится
У вызывающего кода нет стандартных способов определить состояние запущенного async void метода. Как правило их вызов может быть описан фразой "запустил и забыл".
Вызывающий код не может перехватить возникающие исключения
Исключение, выброшенное в async void методе, не будет передано в вызывающий поток. А это значит, что отреагировать на него вызывающий метод не сможет. Например, даже не смотря на отсутствие каких-либо вызовов в GetPageAsync(), исключение не будет перехвачено блоком try-catch.
private async void GetPageAsync(string url)
{
throw new Exception();
}
public void PrintLenght()
{
try {
this.GetPageAsync("http://ya.ru");
Console.WriteLine("Content length: {0}", this._content.Length);
}
catch (Exception ex) {
Console.WriteLine(ex.Message);
}
}
Подстановка async void метода в качестве делегата
Учитывая сказанное ранее, можно отметить, что подстановка async void метода в качестве делегата может привести к непредсказуемым последствиям:
- вызывающий код продолжит выполнение сразу после первого встреченного внутри делегата await;
- перехватить выброшенное исключение не удастся.
Так что же делать с async void?
Ответ очень простой: заменить на async Task. Это позволит вызывающему коду использовать await и контролировать состояние через экземпляр объекта Task. Кроме того, механизм async/await обеспечит возможность обработки выброшенных исключений.
Приведенный ранее в качестве примера код можно переписать так:
private string _content;
private async Task GetPageAsync(string url)
{
var client = new HttpClient();
this._content = await client.GetStringAsync(url);
}
public async void PrintLenght()
{
try {
await this.GetPageAsync("http://ya.ru");
Console.WriteLine("Content length: {0}", this._content.Length);
}
catch (Exception ex) {
Console.WriteLine(ex.Message);
}
}