Byte Array из Stream в C#: 5 лучших практик 2025

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Превращаем 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, который гарантирует чтение

Блоги

Рекомендаций по книгам