Превращаем Stream в Byte Array: гид для C# разработчика (2025 edition)
Вы когда-нибудь ловили себя на мысли: «Ну почему, C#, ты не даёшь мне просто взять и превратить этот Stream в массив байт одной строчкой?». Если да — вы не одиноки. Вопрос «Creating a byte array from a stream» на Stack Overflow собирает тысячи просмотров, и это неспроста. Казалось бы, задача элементарная, но дьявол, как всегда, в деталях: производительность, память, асинхронность и версии .NET.
В 2025 году экосистема .NET предлагает нам несколько элегантных решений. В этой статье мы разберём 5 лучших практик — от классики до новинок. Вы узнаете, когда использовать MemoryStream, почему BinaryReader всё ещё актуален, и как не «уронить» приложение при работе с гигантскими потоками. Поехали!
1. Классика жанра: MemoryStream и CopyTo
Самый популярный и интуитивно понятный способ. Если у вас есть Stream (например, из файла, сети или памяти), вы просто «сливаете» его в MemoryStream, а затем вызываете ToArray().
public static byte[] StreamToBytesUsingMemoryStream(Stream inputStream)
{
using (var memoryStream = new MemoryStream())
{
inputStream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
Плюсы:
- Простота и читаемость.
- Работает во всех версиях .NET (Framework, Core, 5+).
- Поддерживает
CopyToAsyncдля асинхронной работы.
Минусы:
- Если поток огромный (например, 1 ГБ), вы дважды держите данные в памяти: сначала в
MemoryStream, потом в массиве. - Не самый быстрый вариант для очень маленьких потоков (оверхед на создание объекта).
Когда использовать: Для потоков среднего размера (до 100 МБ) и когда читаемость кода важнее микрооптимизаций.
2. Асинхронный герой: ReadAsync и Buffer
В современной разработке асинхронность — стандарт. Метод ReadAsync позволяет читать данные порциями, не блокируя поток. Это особенно полезно в ASP.NET Core или GUI-приложениях.
public static async Task<byte[]> StreamToBytesAsync(Stream inputStream)
{
using (var memoryStream = new MemoryStream())
{
byte[] buffer = new byte[8192]; // 8 КБ — стандартный размер
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await memoryStream.WriteAsync(buffer, 0, bytesRead);
}
return memoryStream.ToArray();
}
}
Совет профессионала: Размер буфера можно менять. Для сетевых потоков 8 КБ — золотая середина. Для SSD-дисков можно увеличить до 64 КБ.
Плюсы:
- Не блокирует вызывающий поток.
- Позволяет обрабатывать данные «на лету» (например, считать хеш).
- Контроль над потреблением памяти.
Минусы:
- Чуть больше кода.
- Требует понимания паттерна async/await.
3. BinaryReader: старый друг лучше новых двух
Многие забывают про BinaryReader, а зря. Он умеет читать весь поток одной командой — ReadBytes. Но есть нюанс: ему нужно знать длину потока заранее.
public static byte[] StreamToBytesUsingBinaryReader(Stream inputStream)
{
if (inputStream.CanSeek)
{
inputStream.Position = 0; // Сброс позиции, если нужно
using (var reader = new BinaryReader(inputStream, Encoding.UTF8, leaveOpen: true))
{
return reader.ReadBytes((int)inputStream.Length);
}
}
else
{
throw new InvalidOperationException("Stream не поддерживает поиск (Seek). Используйте MemoryStream.");
}
}
Важно! Параметр leaveOpen: true (доступен с .NET Core 2.1) предотвращает закрытие исходного потока. Без него BinaryReader закроет Stream после чтения, что часто вызывает баги.
Плюсы:
- Минимум кода.
- Очень быстро для потоков с известной длиной (например,
FileStream).
Минусы:
- Требует
CanSeek == true. Не работает с сетевыми или криптографическими потоками. - Приведение
Lengthкintможет вызвать переполнение для потоков > 2 ГБ.
4. BufferedStream: ускоряем чтение
Если ваш поток «сырой» (например, из сети) и вы планируете читать его маленькими кусочками, оберните его в BufferedStream. Это добавит буферизацию и уменьшит количество системных вызовов.
public static byte[] StreamToBytesBuffered(Stream inputStream)
{
const int bufferSize = 4096;
using (var bufferedStream = new BufferedStream(inputStream, bufferSize))
using (var memoryStream = new MemoryStream())
{
bufferedStream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
Лайфхак: Не используйте BufferedStream, если ваш поток уже буферизирован (например, FileStream в .NET 6+). Это приведёт к двойной буферизации и снижению производительности.
Плюсы:
- Ускорение для «медленных» потоков (сеть, последовательный порт).
- Снижение нагрузки на GC.
Минусы:
- Избыточен для файлов и MemoryStream.
- Требует понимания внутреннего устройства потоков.
5. Новинка .NET 6+: Stream.ReadAtLeast и ReadExactly
В .NET 6 появились методы ReadAtLeast и ReadExactly. Они решают проблему «частичного чтения», когда поток возвращает меньше данных, чем запрошено (например, при работе с сетью).
public static byte[] StreamToBytesUsingReadExactly(Stream inputStream)
{
using (var memoryStream = new MemoryStream())
{
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.ReadAtLeast(buffer, 1, throwOnEndOfStream: false)) > 0)
{
memoryStream.Write(buffer, 0, bytesRead);
}
return memoryStream.ToArray();
}
}
Альтернатива — ReadExactly, который гарантирует чтение