Ответ
ContinueWith — это метод класса Task, который позволяет создать продолжение (continuation): задачу, которая автоматически выполняется после завершения исходной задачи (предшественника), независимо от ее статуса (успех, ошибка, отмена).
Базовый синтаксис:
Task firstTask = Task.Run(() => DoWork());
Task continuationTask = firstTask.ContinueWith(previousTask =>
{
// Этот код выполнится после завершения firstTask
Console.WriteLine($"Первая задача завершена со статусом: {previousTask.Status}");
});
Зачем это нужно?
Для организации цепочек асинхронных операций, где каждая следующая зависит от результата или факта завершения предыдущей, без блокировки потока ожиданием (Wait или Result).
Ключевые параметры TaskContinuationOptions:
С их помощью можно точно контролировать, когда и при каких условиях должно запуститься продолжение.
Task.Run(() => ParseData())
.ContinueWith(prevTask =>
{
// Выполнится ТОЛЬКО если первая задача завершилась успешно (RanToCompletion)
ProcessResult(prevTask.Result);
}, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith(prevTask =>
{
// Выполнится ТОЛЬКО если первая задача была отменена (Canceled)
LogCancellation();
}, TaskContinuationOptions.OnlyOnCanceled)
.ContinueWith(prevTask =>
{
// Выполнится ТОЛЬКО если в первой задаче было исключение (Faulted)
// prevTask.Exception содержит AggregateException
LogError(prevTask.Exception?.InnerException);
}, TaskContinuationOptions.OnlyOnFaulted);
Важные особенности и современная альтернатива:
- Обработка исключений: Исключение из предшественника не приведет к краху продолжения. Оно "упаковывается" в свойство
Exceptionзадачи-продолжения (типAggregateException). - Планировщик задач: Можно указать
TaskScheduler(например,TaskScheduler.FromCurrentSynchronizationContext()для возврата в UI-поток). -
Современная альтернатива —
async/await: В большинстве случаев код сContinueWithменее читаем. Использованиеasync/awaitпредпочтительнее.Старый стиль с
ContinueWith:Task.Factory.StartNew(() => "http://example.com/data.json") .ContinueWith(prev => DownloadStringAsync(prev.Result)) .Unwrap() // Распаковываем вложенную задачу .ContinueWith(prev => JsonConvert.DeserializeObject<Data>(prev.Result));Современный стиль с
async/await:public async Task<Data> GetDataAsync() { string url = await Task.Run(() => "http://example.com/data.json"); string json = await DownloadStringAsync(url); return JsonConvert.DeserializeObject<Data>(json); }
Когда все еще полезен ContinueWith?
- При работе с динамически создаваемыми цепочками задач.
- Для низкоуровневой настройки поведения продолжения (например,
ExecuteSynchronouslyдля выполнения в том же потоке). - В библиотечном коде, где нельзя использовать
async/await.
Ответ 18+ 🔞
А, ну так, ContinueWith... Это ж типа такая штука, которая говорит: «Эй, чувак, как только ты там закончишь свои делишки, не важно, чем это кончилось — успехом, пиздецом или тебя просто отменили — сделай вот это вот сразу после». Как прицеп к поезду, только для задач.
Вот смотри, как это выглядит в коде:
Task firstTask = Task.Run(() => DoWork());
Task continuationTask = firstTask.ContinueWith(previousTask =>
{
// Эта хрень запустится автоматом, когда первая задача отстреляется.
Console.WriteLine($"Первая задача завершена со статусом: {previousTask.Status}");
});
А зачем это вообще надо?
Ну, чтобы не тормозить поток, тупо ожидая, пока первая задача упрется рогом в асфальт. Вместо task.Wait() или task.Result (которые могут заблокировать всё к хуям), ты просто ставишь в очередь следующее действие, которое само запустится, когда придёт время.
Самое сольное — это флаги TaskContinuationOptions.
С ними можно выебать мозг системе и указать точные условия: «Запускай продолжение только если всё было хорошо», или «только если всё пошло по пизде», или «только если операцию отменили».
Task.Run(() => ParseData())
.ContinueWith(prevTask =>
{
// Выполнится ТОЛЬКО если первая задача завершилась успешно.
ProcessResult(prevTask.Result);
}, TaskContinuationOptions.OnlyOnRanToCompletion)
.ContinueWith(prevTask =>
{
// Выполнится ТОЛЬКО если первую задачу отменили.
LogCancellation();
}, TaskContinuationOptions.OnlyOnCanceled)
.ContinueWith(prevTask =>
{
// Выполнится ТОЛЬКО если в первой задаче вылетело исключение.
// Всё дерьмо будет лежать в prevTask.Exception.
LogError(prevTask.Exception?.InnerException);
}, TaskContinuationOptions.OnlyOnFaulted);
Но есть важные моменты, блядь:
- С исключениями — они не похоронят твоё продолжение. Они аккуратно запаковываются в свойство
Exceptionзадачи-продолжения (в этот ёбаныйAggregateException). - Можно выбрать, где запускать — например, вернуться в UI-поток через
TaskScheduler.FromCurrentSynchronizationContext(), чтобы обновить интерфейс. -
А теперь главное: в 2024 году так уже почти не пишут. Есть же
async/await!Вот как это выглядело раньше, в каменном веке:
Task.Factory.StartNew(() => "http://example.com/data.json") .ContinueWith(prev => DownloadStringAsync(prev.Result)) .Unwrap() // Распаковываем вложенную задачу .ContinueWith(prev => JsonConvert.DeserializeObject<Data>(prev.Result));А вот как это делают нормальные люди сейчас:
public async Task<Data> GetDataAsync() { string url = await Task.Run(() => "http://example.com/data.json"); string json = await DownloadStringAsync(url); return JsonConvert.DeserializeObject<Data>(json); }Честно, второй вариант читается в тысячу раз проще. Не надо париться с этими
ContinueWithиUnwrap.
Так когда же ContinueWith ещё может пригодиться?
- Когда ты на лету собираешь цепочки задач, как лего.
- Для каких-то низкоуровневых оптимизаций, вроде
ExecuteSynchronously, чтобы выполнить продолжение в том же потоке. - В каком-нибудь древнем или специфичном библиотечном коде, где
async/awaitнельзя использовать по каким-то идиотским причинам.
Но если ты не пишешь какую-то хардкорную инфраструктуру — просто юзай await. Не усложняй себе жизнь, ёпта.