Какие возвращаемые типы может иметь метод, помеченный ключевым словом async?

Ответ

Асинхронный метод (async) в C# может возвращать только следующие типы, которые представляют собой выполняемую или уже завершенную асинхронную операцию:

  1. Task

    • Назначение: Для асинхронных операций, которые не возвращают значение (аналог void в синхронном мире).
    • Пример: Метод, который что-то сохраняет в базу данных или отправляет уведомление.
      public async Task SaveDataAsync(Data data)
      {
      await _repository.SaveAsync(data);
      _logger.LogInformation("Data saved.");
      }
  2. Task<TResult>

    • Назначение: Для асинхронных операций, возвращающих результат типа TResult.
    • Пример: Метод, запрашивающий данные из API или БД.
      public async Task<Product> GetProductByIdAsync(int id)
      {
      return await _dbContext.Products.FindAsync(id);
      }
  3. ValueTask / ValueTask<TResult>

    • Назначение: Легковесные структуры, которые следует использовать для оптимизации, когда результат операции часто доступен синхронно (например, кэширован в памяти). Они позволяют избежать лишних аллокаций в куче, в отличие от Task.
    • Пример: Метод, который часто возвращает данные из кэша.

      public async ValueTask<string> GetCachedDataAsync(string key)
      {
      if (_cache.TryGetValue(key, out string cachedData))
          return cachedData; // Синхронный возврат, аллокации Task нет!
      
      // Асинхронная загрузка, если нет в кэше
      var data = await _service.FetchDataAsync(key);
      _cache.Set(key, data);
      return data;
      }
  4. void

    • Назначение: Крайне не рекомендуется, кроме одного специфического случая — обработчиков событий (например, button.Click += async (sender, e) => { ... }).
    • Почему плохо: Вызывающий код не может дождаться завершения такой операции (await невозможен) и не сможет перехватить исключения, выброшенные внутри метода. Это приводит к неожиданным сбоям.

Ключевое правило: В 99% случаев используйте Task или Task<T>. Переходите на ValueTask только после профилирования и доказательства, что это дает выгоду в конкретном горячем пути.

Ответ 18+ 🔞

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

Вот смотри, когда ты пишешь async метод, компилятор тебе разрешает вернуть только несколько конкретных штук. И все они, по сути, говорят: "я обещаю когда-нибудь закончиться, а пока не мешай". Вот список этих обещаний:

  1. Task

    • Зачем: Это когда твой метод делает какую-то работу, но в конце не выдаёт тебе никакого результата. Ну, типа сохранил что-то в базу, отправил письмо — и всё, свободен. Аналог обычного void, только асинхронный.
    • Как выглядит:
      public async Task SaveDataAsync(Data data)
      {
      await _repository.SaveAsync(data); // Ждём, пока сохранится
      _logger.LogInformation("Ну всё, сохранил, можно и отдохнуть.");
      }
  2. Task<TResult>

    • Зачем: А вот это уже интереснее. Тут метод не только поработает, но и принесёт тебе из асинхронных далей какой-то результат. Запросил пользователя из БД — получи User. Сходил в API за погодой — получи WeatherForecast. Основная рабочая лошадка.
    • Пример:
      public async Task<Product> GetProductByIdAsync(int id)
      {
      // Найдёт — вернёт, не найдёт — вернёт null, но сделает это асинхронно!
      return await _dbContext.Products.FindAsync(id);
      }
  3. ValueTask / ValueTask<TResult>

    • Зачем: Вот это уже для тех, кто хочет выжимать каждую каплю производительности. Это структуры, а не классы, как Task. Их юзают в очень специфичных случаях, когда твоя операция очень часто завершается моментально, без всякого ожидания. Например, данные уже лежат в кэше в памяти. Чтобы не создавать лишний объект Task в куче просто для того, чтобы сказать "держи, вот твой результат", используют ValueTask. Это оптимизация, не больше.
    • Важное предупреждение: Не лезь с этим в каждый метод! Сначала пиши на обычных Task. Потом, если профилировщик кричит, что у тебя тут аллокации дохуя, и ты точно уверен, что результат часто доступен синхронно, — тогда можно подумать. Иначе нахерачишь себе проблем.
    • Пример, где это может быть уместно:

      public async ValueTask<string> GetCachedDataAsync(string key)
      {
      // О! Данные уже в кэше! Возвращаем мгновенно, без создания Task в куче.
      if (_cache.TryGetValue(key, out string cachedData))
          return cachedData;
      
      // Не повезло, кэш промахнулся. Вот тут уже идём в сеть/диск асинхронно.
      var data = await _service.FetchDataAsync(key);
      _cache.Set(key, data);
      return data;
      }
  4. void

    • Зачем: А вот это, блядь, тёмная сторона силы. Практически никогда не используй это в асинхронных методах. Серьёзно. Есть буквально один легальный случай — обработчики событий. Ну, типа button.Click += async (sender, e) => { await Task.Delay(1000); }.
    • Почему это пиздец? Потому что вызвавший твой async void метод код не может его ни дождаться (await не применить), ни нормально поймать исключения, которые вылетят внутри. Вылетело исключение — и всё, приехали, оно уйдёт в глобальный обработчик и, скорее всего, уронит твое приложение. Доверия к такому коду — ноль ебать.

Итоговая мысль, чтобы не ебал мозг:

  • Не возвращаешь значение — юзай Task.
  • Возвращаешь значение — юзай Task<T>.
  • Стал перфекционистом-оптимизатором после профайлера — посмотри на ValueTask.
  • Хочешь вернуть void — остановись, перепроверь, ты точно пишешь обработчик события? Если нет — иди нахуй, переделывай на Task.