Как сохранить поток (Stream) в файл на C#? Правильные способы и примеры
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.