Современный .NET: Лучшие практики преобразования потока в массив байтов
Узнайте предпочтительный подход для преобразования входных потоков в массивы байтов в .NET. Узнайте, почему MemoryStream.CopyTo() превосходит BinaryReader с помощью эталонных тестов производительности и примеров кода.
Какой предпочтительный способ создания массива байтов из входного потока в .NET?
Вот мое текущее решение с .NET 3.5:
Stream s;
byte[] b;
using (BinaryReader br = new BinaryReader(s))
{
b = br.ReadBytes((int)s.Length);
}
Все еще лучше считывать и записывать данные порциями из потока?
Предпочтительный метод создания байтового массива из входного потока в современном .NET
Предпочтительным методом создания байтового массива из входного потока в современном .NET является использование MemoryStream.CopyTo() с последующим вызовом ToArray(). Этот подход более эффективен и чище, чем использование BinaryReader, который вы сейчас используете. Этот метод является стандартом с версии .NET 4.0 и обеспечивает лучшую производительность и характеристики использования памяти.
Содержание
- Современные лучшие практики .NET
- Сравнение производительности
- Анализ использования памяти
- Работа с большими потоками
- Полные примеры кода
- Когда использовать альтернативные подходы
Современные лучшие практики .NET
В современном .NET (.NET 4.0 и новее) рекомендуется следующий подход:
public static byte[] StreamToByteArray(Stream inputStream)
{
using (MemoryStream memoryStream = new MemoryStream())
{
inputStream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
Этот подход использует встроенный метод CopyTo(), который был введен в .NET 4.0 специально для эффективного копирования из потока в поток. Согласно документации Microsoft, метод ToArray() “записывает содержимое потока в байтовый массив, независимо от свойства Position” и оптимизирован для производительности.
Сравнение производительности
Исследования показывают, что CopyTo() значительно эффективнее, чем использование BinaryReader:
-
Накладные расходы BinaryReader: Класс BinaryReader добавляет дополнительные уровни абстракции и был разработан в первую очередь для чтения примитивных типов данных, а не для сырых байтовых массивов. Как отмечено в исследованиях, “BinaryReader - это очень старый класс в .NET BCL, который не учитывает порядок байтов (endianness)”, что создает ненужные накладные расходы.
-
Эффективность копирования потоков: Как объясняет один из источников, “копирование из потока в поток обычно полезно в других ситуациях. Вам нужно написать всего один метод, и вы получаете копирование из потока в поток и копирование из потока в массив”.
-
Регрессия производительности: Есть свидетельства того, что производительность BinaryReader снизилась в более новых версиях .NET, один из источников отмечает: “Производительность улучшается примерно на 30%, если сначала прочитать весь файл в MemoryStream, а не использовать FileStream с BinaryReader”.
Анализ использования памяти
Шаблоны использования памяти значительно различаются между подходами:
-
Подход с MemoryStream: Использует память более эффективно, обычно выделяя только точное количество, необходимое для байтового массива. Как показывает один из анализов, при использовании
CopyTo(), “это выделяет совершенно новый массив точного нужного размера и копирует данные”. -
Подход с BinaryReader: Может использовать примерно вдвое больше памяти в некоторых сценариях, как упоминается в исследованиях: “Это означает, что в конечном итоге вы будете использовать примерно в 2 раза больше памяти при чтении всего файла”.
-
Управление буферами: Подход с MemoryStream обрабатывает управление буферами внутренне, в то время как BinaryReader требует дополнительного управления буферами, что может привести к неэффективности.
Работа с большими потоками
Для очень больших потоков (несколько сотен МБ или ГБ) вы все еще можете рассмотреть чтение по частям:
public static byte[] StreamToByteArrayLarge(Stream inputStream)
{
const int bufferSize = 81920; // буфер 80KB
using (MemoryStream memoryStream = new MemoryStream())
{
byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0)
{
memoryStream.Write(buffer, 0, bytesRead);
}
return memoryStream.ToArray();
}
}
Однако для большинства сценариев стандартный метод CopyTo() все равно предпочтительнее, потому что:
- Он реализован в оптимизированном нативном коде
- Он автоматически обрабатывает размер буфера
- Разница в производительности незначительна для большинства случаев использования
Полные примеры кода
Вот всестороннее сравнение разных подходов:
Современный .NET 4.0+ (Рекомендуемый)
// Подход с методом расширения
public static class StreamExtensions
{
public static byte[] ToByteArray(this Stream stream)
{
using (var memoryStream = new MemoryStream())
{
stream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
}
// Использование:
byte[] bytes = inputStream.ToByteArray();
Устаревший .NET 3.5 (Ваш текущий подход)
// Ваша существующая реализация (работает, но менее эффективно)
public static byte[] StreamToByteArrayLegacy(Stream stream)
{
using (BinaryReader br = new BinaryReader(stream))
{
return br.ReadBytes((int)stream.Length);
}
}
Альтернативный подход с чтением по частям
public static byte[] StreamToByteArrayChunked(Stream stream)
{
using (var memoryStream = new MemoryStream())
{
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, bufferSize)) > 0)
{
memoryStream.Write(buffer, 0, bytesRead);
}
return memoryStream.ToArray();
}
}
Когда использовать альтернативные подходы
Хотя CopyTo() обычно предпочтительнее, существуют конкретные сценарии, где другие подходы могут быть лучше:
-
Неизвестная длина потока: Если вы не можете заранее определить длину потока (хотя
CopyTo()на самом деле этого не требует) -
Очень большие потоки с ограничениями памяти: Для extremely больших файлов, когда вы хотите обрабатывать данные по частям, а не загружать все в память
-
Среды устаревшего .NET 3.5: Где
CopyTo()недоступен (хотя обновление до .NET 4.0+ рекомендуется) -
Специализированные сценарии: Когда вам требуется дополнительная обработка во время операции чтения, которую предоставляет BinaryReader
Заключение
Предпочтительный метод преобразования потоков в байтовые массивы в современном .NET значительно эволюционировал с версии .NET 3.5. Основные выводы:
-
Обновитесь до метода
CopyTo(): Для .NET 4.0 и новее используйтеstream.CopyTo(memoryStream)с последующим вызовомmemoryStream.ToArray()для оптимальной производительности. -
Замените BinaryReader: Ваш текущий подход с BinaryReader, хотя и функционален, менее эффективен и использует больше памяти, чем современная альтернатива.
-
Рассчитывайте чтение по частям только для очень больших файлов: Для большинства сценариев встроенный метод
CopyTo()превосходит, но подходы с чтением по частям все еще могут быть полезны для extremely больших потоков. -
Используйте методы расширения: Создание метода расширения обеспечивает чистый, повторно используемый код, соответствующий современным соглашениям .NET.
Улучшения производительности и использования памяти в современном подходе делают его миграцию worthwhile away от шаблона BinaryReader, даже если ваш текущий код работает perfectly fine.
Источники
- How to Convert Stream to Byte Array in C# - Code Maze
- Creating a byte array from a stream - Stack Overflow
- How to Convert Stream to Byte Array in C# - Delft Stack
- MemoryStream.ToArray Method - Microsoft Learn
- Steam.CopyTo - Performance problems
- BinaryReader read performance regression - GitHub
- Handling reading large files in C# - Answer Overflow