У вас проект на .NET 4.0 и вам надоела «лапша» из колбеков? Вы бы хотели использовать async/await в своем проекте, но тимлид грозит небесной карой за смену платформы? Если установка патча на фреймворк и студию для вас являются допустимым решением, то вам сюда. Если нет, то существует другое решение.
Немного теории
Async-метод это сопрограмма, которая выходит на каждом await, и восстанавливается с этой точки по завершению ожидания(автор знает, что выполнение не всегда прерывается на await и что «ожидать» можно не только экземпляры Task). Итак, начиная со второй версии дотнета можно легко создавать сопрограммы с помощью ключевого слова yield. Этим инструментом мы и воспользуемся.
Концепт
Хочется писать как на C# 5.0, и при этом не ставить никаких языковых расширений. К сожалению так не получится. Но есть вот такой вариант:
private IEnumerable Login(...)
{
// ...
// get user id, if not specified
if (string.IsNullOrEmpty(uid))
{
var getUserIdTask = сlient.GetUserId(...); yield return getUserIdTask; // await
uid= getUserIdTask.Result.uid;
}
// login
var loginTask = сlient.Login(...); yield return loginTask; // await
var sessionId = loginTask.Result.SessionId;
// getting user's profile
var getUserInfoTask = сlient.GetUserInfo(...); yield return getUserInfoTask; // await
var userInfo = getUserInfoTask.Result;
// ...
yield return userInfo; // return
}
Всё что возвращается через yield return и не является наследником Task считается результатом исполнения async-метода.
Реализация
Код лежит тут.
Механизм работы простой:
- Создаем корневой Task и возвращаем вызывающему
- Вращаем итератор
- Если вернулся Task, то ждем его завершения через ContinueWith с переходом к шагу №2
- Если вернулась ошибка, то выставляем Exception для коневого Task
- Если вернулось значение, то завершаем коневой Task с данным результатом
- Если итератор кончился, то завершаем коневой Task со стандартным результатом
Во всех вариантах завершения на итераторе будет вызван Dispose, что приведет к освобождению ресурсов в блоках using и try/finally.
Начать новую асинхронную задачу можно вызвав метод FromIterator:
private IEnumerable Login(...) { ... }
Task loginTask = TaskUtils.FromIterator(this.Login(...));
// или с возвращаемым значением
Task<UserInfo> loginTask = TaskUtils.FromIterator<UserInfo>(this.Login(...));
Опционально можно указать:
- state — состояние, которое попадет в Task.AsyncState
- creationFlags — TaskCreationOptions для корневой задачи. Установка TaskCreationOptions.LongRunning говорит о том что вы очень не хотите блокировать текущий поток и вся работа должна быть выполнена в другом потоке
- cancellationToken — токен прерывания процесса исполнения async-метода, передается вглубь везде, где это возможно
Заключение
Плюсы:
- Компактненько и чисто
- Можно не бояться за unmanaged ресурсы, using, try/catch работают
- Не требует патчей и доп. библиотек
- Будет работать в Mono
Минусы:
- Async-метод возвращает IEnumerable, а не Task. Иногда приходится создавать дополнительный метод, который возвращает Task.
- «Ожидать» можно только экземпляры Task
- Ошибки «ожидаемых» задач нельзя обработать через try/catch(можно через ContinueWith). При ошибке в ожидаемой задаче, у корневой задачи выставляется Exception и поток исполнения больше не посещает async-метод.
- Внутренний класс реализации(TaskBuilder) не порождает ссылок на себя, кроме тех случаев когда подписывается на ContinueWith в ожидаемых задачах, и есть вероятность сборки его GC
Большую часть минусов можно побороть тем или иным способом.
Ошибки по тексту можно направлять в ЛС.
Исходный код.
Автор: shai_hulud