В каких случаях возвращать void, а в каких Task в C#?

Ответ

Выбор между void и Task определяется необходимостью асинхронности и ожидания.

Используйте void для:

  • Синхронных методов, которые не выполняют операций ввода-вывода (I/O) или длительных вычислений.
  • Обработчиков событий (event handlers), так как их сигнатура определена делегатом и не поддерживает await. Исключения здесь должны обрабатываться внутри метода.

Используйте Task (или Task<T>) для:

  • Асинхронных методов, даже если они не возвращают результат (Task). Это позволяет вызывающей стороне:
    • Ожидать завершения операции с помощью await.
    • Правильно обрабатывать исключения, которые "всплывают" из метода.
    • Композировать асинхронные операции (например, с Task.WhenAll).
    • Контролировать состояние задачи (отмена через CancellationToken).

Примеры:

// void – синхронный обработчик события
public void OnButtonClicked(object sender, EventArgs e)
{
    // Синхронная работа
    Log("Button clicked");
}

// Task – асинхронная операция без возвращаемого значения
public async Task ProcessDataAsync(CancellationToken cancellationToken)
{
    await Task.Delay(1000, cancellationToken); // Асинхронное ожидание
    await _fileService.SaveAsync("data.json", cancellationToken); // Асинхронный I/O
    Console.WriteLine("Data processed");
}

Ключевое правило: Преимущественно используйте Task для всех операций, связанных с I/O, сетью или длительными вычислениями. void оставьте для синхронных обработчиков событий.

Ответ 18+ 🔞

Давай разжую эту тему про void и Task, чтобы понятно стало, как будто на пальцах объясняю. Ситуация, в общем-то, проще пареной репы, если не усложнять.

Представь, что у тебя есть два друга. Один — это void, мужик простой, прямолинейный, но немного туповатый. Сказал ему «сделай» — он пошёл, сделал, и ни хуя тебе не доложит. Ни «готово», ни «ой, я там обосрался». Просто молчок. Если внутри у него пиздец случится и исключение выскочит, то оно, как необъяснимый поджопник, прилетит в самое неожиданное место, и ты даже не поймёшь, откуда ноги растут.

Второй друг — Task. Это уже парень поумнее, с коммуникацией. Сказал ему «сделай» — он кивает: «Без проблем, я в процессе». Ты можешь спокойно ждать его у подъезда с бутылкой (await), можешь параллельно другим делом заняться, а он тебе в итоге чётко скажет: «Всё, готово» или «Чувак, тут проблема, лови исключение». С ним как-то надёжнее.

Так когда кого звать?

Зови void только в двух случаях, иначе будешь выглядеть как полный чайник:

  1. Когда метод синхронный и быстрый. Ну типа посчитать что-то в уме, локальную переменную поменять. Никаких походов в интернет, в базу данных, в файловую систему — ничего, что может заставить ждать. Быстро чирик — и свободен.
  2. Когда пишешь обработчик события (event handler). Это вынужденная жертва, потому что старые делегаты так устроены, они на void подписаны. Но тут важно — всю свою внутреннюю асинхронную поебень ты обязан внутри этого метода переварить сам. Выплюнул исключение наружу — и вся твоя программа накрылась медным тазом, потому что ловить его будет некому. Ответственность, блядь, полная.
// Вот типичный void-обработчик. Смирись.
public void Button1_Click(object sender, EventArgs e)
{
    // Делаем что-то простое и синхронное
    label1.Text = "Кликнули, ёпта!";
}

А теперь Task — это твой основной инструмент, когда дело пахнет асинхронностью:

  • Идешь за чем-то в сеть? Task.
  • Читаешь/пишешь файл? Task.
  • Ждёшь ответа от базы данных? Task, мать его.
  • Даже если метод в итоге ничего не возвращает (просто void-работа), но делает её асинхронно — всё равно возвращай Task. Это как квитанция, что работа принята в обработку.

Почему Task — это охуенно?

  • Можно ждать (await). Не будешь же ты тыкаться в экран, пока данные грузятся.
  • Исключения не теряются. Они аккуратно упаковываются в задачу и прилетают прямо тебе в руки при await или в try-catch.
  • Можно делать несколько дел сразу. Task.WhenAll — это просто песня. Отправил пачку запросов параллельно и ждёшь, когда все отзовутся.
  • Можно вежливо попросить остановиться. Через CancellationToken скажешь «всё, не надо» — и задача постарается завершиться по-хорошему.
// А вот это уже Task — работа с ожиданием.
public async Task DownloadAndSaveFileAsync(string url, CancellationToken ct)
{
    // Ждём сеть — не блокируя весь интерфейс
    var data = await _httpClient.GetByteArrayAsync(url, ct);
    // Ждём диск
    await File.WriteAllBytesAsync("file.zip", data, ct);
    // Всё, доложили о выполнении.
    Console.WriteLine("Файл скачан, можно расслабиться.");
}

Итог, чтобы не ебать мозг: Забудь про void для любой работы, которая хоть как-то может зависнуть. По умолчанию все не-событийные методы, которые что-то ждут, делай async Task. А void оставь для старых событийных протоколов и простых синхронных действий, которые выполняются быстрее, чем ты успеваешь моргнуть. Всё, вопрос закрыт.