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

«Какие возвращаемые типы может иметь метод, помеченный ключевым словом async?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Асинхронный метод (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 только после профилирования и доказательства, что это дает выгоду в конкретном горячем пути.