Para convertir un stream en un array de bytes en C# tenemos que leer el stream hasta el final, porque un Stream es un cursor sobre una secuencia de bytes y no un contenedor de ellos. Tiene una posición, entrega bytes a partir de esa posición y, en general, no sabe cuántos quedan.

Para descargar el código fuente de este artículo, puedes visitar nuestro repositorio de GitHub.

En .NET 7 y versiones posteriores, basta una sola llamada cuando el stream ofrece un Length utilizable: crea un array de ese tamaño y llénalo con ReadExactly(). Cuando la longitud es desconocida, copia el stream en un MemoryStream con CopyTo() y llama a ToArray().

Todo lo que sigue es el detalle que hay detrás de esas dos líneas: por qué no basta con una sola llamada a Read(), cuánto cuesta cada uno de BinaryReader, StreamReader, MemoryStream y BufferedStream, y qué dice el benchmark.

Preparación del origen de datos

Para empezar, vamos a agregar un archivo de texto al proyecto:

According to a report by CMGSMA, the mobile industry in Codemaze City has seen
mobile broadband connections grow from 10% in 2000 to 74% in 2023.

Ahora, vamos a usar este archivo para crear un objeto FileStream:

private static readonly string _sampleFilePath
    = Path.Combine(Directory.GetParent(Directory.GetCurrentDirectory()).Parent.Parent.FullName, "samplefile.txt");

private readonly FileStream _stream
    = new(_sampleFilePath, FileMode.Open, FileAccess.Read);

Nuestro objetivo es implementar métodos que conviertan _stream en un array de bytes.

Ese es el entorno de prueba, que lee un archivo de ejemplo de 145 bytes. El benchmark del final del artículo se ejecuta sobre un archivo distinto y mucho más grande, un benchmarkfile.txt de unos 718 KB creado repitiendo la línea anterior, y cada método del benchmark abre su propio FileStream nuevo para que cada iteración empiece en la posición cero.

¿Cuál es la forma más sencilla de convertir un stream en un array de bytes en C#?

Un stream no conoce su propia longitud de forma fiable, así que convertirlo en un array de bytes implica leer hasta que se acaben los datos. Dos llamadas resuelven casi todos los casos.

Cuando el stream admite posicionamiento (seekable) y ofrece un Length fiable, crea un único array de ese tamaño y llénalo con ReadExactly(). Este método repite la lectura internamente hasta llenar el búfer y lanza EndOfStreamException si el stream termina antes, de modo que una lectura incompleta se convierte en un error en lugar de en un array truncado sin ningún aviso.

Cuando la longitud es desconocida, algo habitual en los streams de red, de tuberías (pipes) y comprimidos, copia el stream en un MemoryStream con CopyTo() y llama a ToArray() sobre él. Esto cuesta una segunda copia de los datos en memoria y no necesita la longitud para nada.

Ambos parten de la posición actual del stream. En un stream del que ya se ha leído algo, CopyTo() solo devuelve lo que queda y ReadExactly() lanza una excepción, porque el stream termina antes de que el array esté lleno, así que rebobinarlo antes es responsabilidad del código que llama.

Vamos a empezar por el caso en que conocemos la longitud:

public byte[] UseReadExactly(Stream stream)
{
    var bytes = new byte[stream.Length];
    stream.ReadExactly(bytes);

    return bytes;
}

Y cuando la longitud es desconocida, dejamos que CopyTo() se encargue de la lectura:

public byte[] UseCopyTo(Stream stream)
{
    using var memoryStream = new MemoryStream();
    stream.CopyTo(memoryStream);

    return memoryStream.ToArray();
}

Si el stream es un archivo que hemos abierto nosotros mismos, no necesitamos ningún stream: File.ReadAllBytes() lee el archivo completo en una sola llamada, y el artículo sobre cómo convertir un archivo en un array de bytes trata ese caso.

¿Cómo convertir un stream con Stream.Read()?

Stream.Read() puede devolver menos bytes de los que pedimos. El valor devuelto es el número de bytes que realmente colocó en el búfer, y ese número es una promesa de progreso, no de finalización. Un stream de archivo puede devolver un bloque de disco, y un stream de red devuelve lo que haya llegado hasta ese momento.

Por eso, una sola llamada nunca basta. El bucle siguiente llama a Read() una y otra vez hasta que devuelve cero, que es la única señal de que el stream ya no tiene más datos que entregar.

El runtime lo hace por nosotros. ReadExactly() realiza internamente la misma lectura repetida y lanza una excepción cuando el stream termina antes de tiempo, así que el bucle escrito a mano está aquí como muestra de lo que ahora hace el framework, no como código que haya que copiar.

La parte costosa no es el bucle. Es la List<byte> y el búfer de 32 bytes, y por eso este método termina el último en el benchmark.

Un stream de doce bytes leído mediante cuatro llamadas sucesivas a Read que devuelven cinco, cinco, dos y cero bytes.

Este es el mecanismo, no la recomendación. En .NET 7 y versiones posteriores, usa ReadExactly() si conoces la longitud.

Para asegurarnos de leer y convertir con precisión todos los datos del stream con el método Stream.Read(), podemos crear un método que lea y copie los datos repetidamente hasta que no quede nada más por leer:

public byte[] UseStreamDotReadMethod(Stream stream)
{
    byte[] bytes;
    List<byte> totalStream = new();
    byte[] buffer = new byte[32];
    int read;

    while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
    {
        totalStream.AddRange(buffer.Take(read));
    }

    bytes = totalStream.ToArray();

    return bytes;
}

En este método, empezamos inicializando totalStream, un objeto List<byte>, y un array de bytes de tamaño 32 que hace de búfer.

A continuación, definimos read, una variable entera que almacenará el número de bytes leídos del stream. Después usamos un bucle while para leer datos del stream en el array del búfer, una y otra vez, hasta que no queden más bytes por leer.

En la condición del bucle while, comprobamos el valor de la variable read. Si es mayor que cero, agregamos los bytes leídos a la lista totalStream con el método AddRange(), que los coloca al final de la lista. Usamos el método Take() para seleccionar solo la cantidad de bytes leídos y no agregar a la lista totalStream los bytes de una lectura anterior que siguen en el búfer.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Por último, devolvemos el contenido de la lista totalStream como un array de bytes mediante el método ToArray().

¿Cómo convertir un stream con BinaryReader?

Una forma más sencilla de convertir un stream en un array de bytes en C# es usar la clase BinaryReader. Esta clase nos permite leer datos binarios de un stream y convertirlos en un array de bytes. A diferencia de Stream.Read(), BinaryReader.ReadBytes() sigue leyendo hasta obtener la cantidad de bytes que pedimos o hasta que el stream termina, así que basta con una llamada.

Para ver cómo funciona, vamos a crear un método UseBinaryReader():

public byte[] UseBinaryReader(Stream stream)
{
    byte[] bytes;

    using (var binaryReader = new BinaryReader(stream))
    {
        bytes = binaryReader.ReadBytes((int)stream.Length);
    }

    return bytes;
}

Aquí, creamos un objeto BinaryReader con el stream que queremos leer. A continuación, usamos el método ReadBytes() de la clase BinaryReader para leer los bytes del stream y convertirlos en un array de bytes.

El método ReadBytes() recibe un parámetro entero que indica el número de bytes que se deben leer del stream. En este caso, queremos leer todos los bytes del stream. Por eso, le pasamos la longitud del stream como argumento.

Este método necesita un stream que ofrezca su Length, y la conversión explícita a int lo limita a unos 2 GB, así que no es el método adecuado para un stream de red ni para un archivo muy grande.

Por último, devolvemos el array de bytes que contiene los datos del stream.

¿Cómo convertir un stream con StreamReader?

Como alternativa, podemos convertir un stream en un array de bytes con la clase StreamReader. Esta clase nos permite leer fácilmente datos de texto de un stream; después, convertimos esos datos en un array de bytes.

Para usar la clase StreamReader con este fin, vamos a definir un método UseStreamReader().

Lo incluimos para que la lista esté completa, pero no debe usarse para obtener bytes. Decodifica el stream como texto y lo vuelve a codificar, lo que altera sin ningún aviso cualquier dato que no sea texto UTF-8 sin marca de orden de bytes (BOM):

public byte[] UseStreamReader(Stream stream)
{
    byte[] bytes;

    using (var reader = new StreamReader(stream))
    {
        bytes = System.Text.Encoding.UTF8.GetBytes(reader.ReadToEnd());
    }

    return bytes;
}

Primero, creamos una instancia de la clase StreamReader y le pasamos el stream original como argumento. Después, llamamos al método ReadToEnd() del lector para leer todos los datos del stream como una cadena.

Por último, usamos el método GetBytes() de la clase de codificación UTF8 para convertir la cadena en un array de bytes y devolverlo al código que llama.

Este método supone que el stream contiene datos de texto codificados en UTF-8. Si los datos usan otra codificación, deberíamos pasar la codificación correspondiente al constructor de StreamReader y usarla en lugar de UTF8, y si lo que tenemos en realidad es una cadena y no un stream, convertir una cadena en un array de bytes es el camino más corto.

¿Cómo convertir un stream con MemoryStream?

Además, podemos usar la clase MemoryStream para convertir streams en arrays de bytes. Esta clase funciona con streams de longitud desconocida, a costa de consumir memoria adicional durante la conversión, sobre todo cuando trabajamos con streams de datos grandes.

CopyTo() mueve los datos bloque a bloque, así que el stream de origen nunca se guarda completo en un búfer propio. Aun así, el propio MemoryStream crece hasta contener todo el payload, duplicando su capacidad a medida que avanza, y después ToArray() lo vuelve a copiar; por eso este método asigna más memoria que uno que crea el array con su tamaño desde el principio.

Con esto en mente, vamos a definir un método UseMemoryStream():

public byte[] UseMemoryStream(Stream stream)
{
    byte[] bytes;

    if (stream is MemoryStream memStream)
    {
        return memStream.ToArray();
    }

    using (var memoryStream = new MemoryStream())
    {
        stream.CopyTo(memoryStream);
        bytes = memoryStream.ToArray();
    }

    return bytes;
}

Aquí, usamos una instrucción if para comprobar si el stream que queremos convertir ya es un MemoryStream. Si lo es, llamamos al método ToArray() del stream y devolvemos el array de bytes resultante.

En cambio, si el stream no es un MemoryStream, creamos una nueva instancia de la clase MemoryStream. Después, usamos el método CopyTo() para copiar el contenido del stream original en el stream en memoria. Luego invocamos el método ToArray() del stream en memoria para obtener el stream representado como array de bytes.

¿Cómo convertir un stream con BufferedStream?

Ahora, veamos otro método que usa la clase BufferedStream.

La clase BufferedStream permite mejorar el rendimiento de un stream incorporando un búfer.

Usamos un búfer para almacenar datos temporalmente cuando se transfieren entre componentes de un sistema informático que funcionan a velocidades distintas. Este almacenamiento momentáneo mejora la eficiencia del procesamiento y la transferencia de datos, porque reduce el número de llamadas entre los componentes del sistema. Sin embargo, al convertir un stream en un array de bytes con CopyTo(), la copia ya lee en bloques grandes, así que un búfer adicional no reduce el tiempo total de lectura del stream.

Para convertir un stream en un array de bytes con la clase BufferedStream, vamos a empezar definiendo un método:

public byte[] UseBufferedStream(Stream stream)
{
    byte[] bytes;

    using (var bufferedStream = new BufferedStream(stream))
    {
        using var memoryStream = new MemoryStream();
        bufferedStream.CopyTo(memoryStream);
        bytes = memoryStream.ToArray();
    }

    return bytes;
}

Antes que nada, declaramos una variable para guardar los datos que vamos a convertir. Luego creamos una instancia de la clase BufferedStream y le pasamos el stream original como argumento.

A continuación, creamos un objeto MemoryStream para escribir los datos que leemos del objeto BufferedStream. Para transferir los datos entre los dos streams, usamos el método CopyTo() del objeto BufferedStream y le pasamos el objeto MemoryStream como argumento.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Esta transferencia es necesaria porque un BufferedStream no tiene ningún método que devuelva su contenido como un array. Por eso, para convertir el contenido del BufferedStream en un array de byte[] sin conocer su longitud, primero tenemos que copiar los datos en un búfer intermedio, como un MemoryStream, y después convertirlo en un array de bytes.

Por último, invocamos el método ToArray() del objeto MemoryStream para obtener los datos como un array de bytes y devolverlo al código que llama.

¿Cuál es la forma más rápida de convertir un stream en un array de bytes en C#?

Ahora que todos los métodos están listos, podemos evaluar su rendimiento en tiempo y en memoria con un benchmark.

Ya podemos ejecutar las pruebas de benchmark y examinar los resultados en la consola:

| Method                 | Mean       | Error    | StdDev   | Allocated  |
|----------------------- |-----------:|---------:|---------:|-----------:|
| UseReadExactly         |   350.3 us | 10.69 us | 30.84 us |  718.14 KB |
| UseBinaryReader        |   364.2 us |  9.59 us | 27.35 us |   718.2 KB |
| UseBufferedStream      |   433.3 us |  8.93 us | 26.34 us | 2639.37 KB |
| UseCopyTo              |   439.2 us | 15.40 us | 45.41 us | 2639.44 KB |
| UseMemoryStream        |   448.5 us | 13.57 us | 40.01 us | 2639.42 KB |
| UseStreamReader        |   934.8 us | 28.22 us | 82.76 us | 3605.96 KB |
| UseStreamDotReadMethod | 1,567.8 us | 30.88 us | 30.33 us | 3847.81 KB |

Los números proceden de la biblioteca BenchmarkDotNet, y la implementación del benchmark está en nuestro repositorio.

Según los resultados del benchmark, vemos que UseReadExactly() es el método más rápido para convertir un stream en un array de byte[] en C#, y además es el que menos memoria asigna: un array del tamaño del payload y nada más.

UseBinaryReader() queda en segundo lugar, alrededor de un 4 % por detrás, y asigna el mismo array único. Sigue siendo el método al que recurrir cuando el código ya está leyendo valores binarios tipados del stream, porque el lector ya está ahí de todos modos.

Después vienen UseBufferedStream(), UseCopyTo() y UseMemoryStream(), que forman un solo grupo y no tres puestos distintos: sus márgenes de error se solapan, y los tres asignan unos 2639 KB, aproximadamente 3.7 veces lo que asignan los dos métodos que los preceden. Esa diferencia se debe a que el MemoryStream duplica su búfer hasta poder contener el payload, antes de que ToArray() lo vuelva a copiar.

UseStreamReader() tarda 934.8 us, unas 2.7 veces lo que tarda el ganador, y asigna 3606 KB, la segunda cifra más alta de la tabla. Eso es lo que cuesta decodificar cada byte como texto y volver a codificarlo.

Por último, el método más lento en este benchmark es UseStreamDotReadMethod(). Tarda unas 4.5 veces lo que el ganador y es el que más memoria asigna de los siete, que es el precio de hacer crecer una List<byte> de 32 en 32 bytes.

Aun así, la velocidad es solo uno de los criterios, y el que suele decidir la cuestión es si el stream conoce su propia longitud:

MétodoNecesita conocer LengthÚsalo cuandoTen en cuenta
Stream.ReadExactly()SíEl stream admite posicionamiento y quieres una sola asignaciónLanza EndOfStreamException si el stream termina antes de tiempo
Stream.CopyTo() en un MemoryStreamNoLa longitud es desconocida: red, tuberías, datos comprimidosGuarda el payload dos veces, una en el stream y otra en el array
BinaryReader.ReadBytes()SíYa estás leyendo valores binarios tipadosLa conversión (int) limita el stream a unos 2 GB
StreamReader más Encoding.GetBytes()NoNunca, si lo que quieres son bytesDecodifica como texto y vuelve a codificar, lo que corrompe todo lo que no sea texto UTF-8 sin marca de orden de bytes
BufferedStream sobre el stream de origenNoEl origen hace muchas lecturas pequeñas sin búferAgrega un búfer encima de un CopyTo() que ya tiene el suyo
File.ReadAllBytes()No aplicaEl origen es una ruta de archivo, no un streamNo es un método de stream: consulta el artículo sobre cómo convertir un archivo en un array de bytes

En resumen, UseReadExactly() es la mejor opción para convertir un stream en un array de byte[] en C#, dados sus bajos requisitos de tiempo y memoria. Y cuando el array se crea para registrarlo en un log, imprimirlo o pegarlo en algún lugar, convertir esos bytes en algo que una persona pueda leer es el siguiente paso.

¿Cómo convertir un array de bytes de nuevo en un stream en C#?

La conversión en sentido contrario es más barata, porque un array de bytes ya contiene justo los datos que entrega un stream. Envolverlo en un MemoryStream no copia nada: el stream lee directamente del array que le pasamos.

Hay dos constructores que importan. new MemoryStream(bytes) envuelve el array en un stream de tamaño fijo y no redimensionable, así que no puede crecer más allá del array que recibió y escribir más allá del final falla. new MemoryStream(), seguido de escrituras, crea un stream expandible con su propio búfer, que es lo que queremos cuando aún van a llegar más datos.

Hay un detalle que hace tropezar a mucha gente. Un stream en el que acabamos de escribir queda posicionado al final de sus datos, así que quien lo lea a continuación no ve nada en absoluto. La solución es volver a dejar Position en cero antes de pasar el stream, y olvidarlo es la causa habitual de un resultado vacío.

Vamos a envolver un array que ya tenemos:

public Stream UseMemoryStreamConstructor(byte[] bytes)
{
    return new MemoryStream(bytes);
}

Como ese stream lee directamente de nuestro array, las escrituras también van en sentido contrario: escribir dentro de sus límites modifica directamente el array del código que llama, y escribir más allá del final lanza NotSupportedException con el mensaje “Memory stream is not expandable.”.

Cuando aún van a llegar más datos, necesitamos la forma expandible:

public Stream UseWritableMemoryStream(byte[] bytes)
{
    var stream = new MemoryStream();
    stream.Write(bytes, 0, bytes.Length);
    stream.Position = 0;

    return stream;
}

Aquí escribimos el array completo en un stream que tiene su propio búfer y después lo rebobinamos. Y volver a escribir ese array equivale a convertir un array de bytes de nuevo en un archivo.

La clase ofrece más que estos dos constructores, y el artículo sobre cómo usar MemoryStream en C# explica el resto.

Conclusión

En este artículo, hemos visto siete métodos para convertir un stream en un array de bytes en C# y dos formas de hacer la conversión inversa. Además, hemos realizado pruebas de benchmark para evaluar la velocidad y el uso de memoria de todos los métodos, y ReadExactly() ha quedado primero en ambos aspectos.

Probado con .NET 10.0.10 y BenchmarkDotNet 0.15.8.