Avatar ·

Как сохранить поток (Stream) в файл на C#? Правильные способы и примеры

Всем привет! Я начинающий C# разработчик и столкнулся с такой задачей: у меня есть объект типа `Stream` (например, я получил его из HTTP-запроса или из памяти), и мне нужно сохранить все его содержимое в файл на диске. Сначала я попробовал самый простой способ: прочитать поток в массив байтов с помощью `Read()` и потом записать через `File.WriteAllBytes`. Вот мой код:
public void SaveStreamToFile(Stream inputStream, string filePath)
{
    byte[] buffer = new byte[inputStream.Length];
    inputStream.Read(buffer, 0, buffer.Length);
    File.WriteAllBytes(filePath, buffer);
}
Но этот код падает с ошибкой, если поток не поддерживает позиционирование (например, сетевой поток). Кроме того, я читал, что у больших файлов могут быть проблемы с памятью. Подскажите, пожалуйста, как правильно и эффективно сохранить произвольный поток (не зная заранее его длину) в файл? Какие есть best practices? Нужно ли закрывать поток после записи? Заранее спасибо за помощь! <<>>

Классическое решение: копирование через буфер

Самый правильный и универсальный способ — это копировать данные из исходного потока в файловый поток небольшими блоками. Такой подход работает с любыми потоками (сетевыми, memory stream, gzip и т.д.) и не требует загрузки всего содержимого в память.

Основной инструмент — метод Stream.CopyTo() (доступен с .NET Framework 4.5 / .NET Core 1.0) или его асинхронная версия CopyToAsync().

public void SaveStreamToFile(Stream sourceStream, string filePath)
{
    // Убедимся, что директория существует
    string? directory = Path.GetDirectoryName(filePath);
    if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
        Directory.CreateDirectory(directory);

    using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
    {
        sourceStream.CopyTo(fileStream);
    }
}

Асинхронная версия (рекомендуется для UI и серверов)

public async Task SaveStreamToFileAsync(Stream sourceStream, string filePath)
{
    using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
    {
        await sourceStream.CopyToAsync(fileStream);
    }
}

Что происходит под капотом?

  • FileMode.Create — создаёт новый файл или перезаписывает существующий.
  • using — гарантирует, что файловый поток будет закрыт и освобождён, даже если возникнет исключение.
  • CopyTo() — читает из sourceStream блоками (по умолчанию 81920 байт) и записывает их в fileStream.
  • Асинхронность — не блокирует поток, что критично для UI-приложений и высоконагруженных серверов.

Если нужно указать свой размер буфера

sourceStream.CopyTo(fileStream, bufferSize: 65536); // 64 KB

Важные замечания

  • Не используйте inputStream.Length — многие потоки (сетевые, gzip) не поддерживают свойство Length и вызовут NotSupportedException.
  • Не используйте Read() без цикла — Read() может вернуть меньше байт, чем запрошено (особенно в сетевых потоках).
  • Закрывать исходный поток или нет? — CopyTo() не закрывает sourceStream. Если вы его больше не используете — закройте явно или оберните в using.

Полный пример с обработкой ошибок

public async Task TrySaveStreamAsync(Stream sourceStream, string filePath)
{
    try
    {
        using (sourceStream) // закроем исходный поток после копирования
        using (FileStream fs = new FileStream(filePath, FileMode.Create))
        {
            await sourceStream.CopyToAsync(fs);
        }
        Console.WriteLine("Файл сохранён успешно.");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Ошибка при сохранении: {ex.Message}");
    }
}

Этот подход — золотой стандарт в C#. Он безопасен, эффективен и работает с любыми типами потоков.

<<>>

Альтернативный подход: ручное копирование с буфером

Если по каким-то причинам вы не можете использовать CopyTo() (например, работаете с очень старой версией .NET), можно реализовать копирование вручную. Это также полезно для понимания того, как работает потоковая передача данных.

public void SaveStreamManually(Stream sourceStream, string filePath)
{
    const int bufferSize = 8192; // 8 KB — оптимальный размер для большинства сценариев
    byte[] buffer = new byte[bufferSize];
    int bytesRead;

    using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
    {
        // Читаем, пока поток не вернёт 0 (конец данных)
        while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
        {
            fileStream.Write(buffer, 0, bytesRead);
        }
    }
}

Когда это может пригодиться?

  • В легаси-проектах на .NET Framework 4.0 и ниже.
  • Если нужно логировать прогресс (например, выводить процент выполнения).
  • Для кастомной обработки данных «на лету» (например, подсчёт хеша или шифрование).

Обратите внимание: этот код корректно обрабатывает ситуацию, когда Read() возвращает меньше байт, чем размер буфера — в отличие от вашего первоначального варианта с inputStream.Read(buffer, 0, buffer.Length).

В современных проектах всё же рекомендую использовать CopyTo() — он короче, понятнее и оптимизирован Microsoft.