Para converter um stream em um array de bytes em C#, precisamos ler o stream até o fim, porque um Stream é um cursor sobre uma sequência de bytes, e não um contêiner que os guarda. Ele tem uma posição, entrega os bytes a partir dessa posição e, no caso geral, não sabe quantos ainda restam.

Para baixar o código-fonte deste artigo, você pode acessar nosso repositório no GitHub.

No .NET 7 e versões posteriores, uma única chamada resolve quando o stream informa um Length utilizável: aloque um array desse tamanho e preencha-o com ReadExactly(). Quando o tamanho é desconhecido, copie o stream para um MemoryStream com CopyTo() e chame ToArray().

Tudo o que vem a seguir é o detalhe por trás dessas duas linhas: por que uma única chamada a Read() não basta, quanto custa cada um de BinaryReader, StreamReader, MemoryStream e BufferedStream, e o que diz o benchmark.

Preparação da fonte de dados

Para começar, vamos adicionar um arquivo de texto ao projeto:

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.

Agora, vamos usar esse arquivo para criar um 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);

Nosso objetivo é implementar métodos que convertam _stream em um array de bytes.

Essa é a fixture de teste, e ela lê um arquivo de exemplo de 145 bytes. O benchmark no fim do artigo usa um arquivo diferente e muito maior, um benchmarkfile.txt de aproximadamente 718 KB criado com a repetição da linha acima, e cada método do benchmark abre seu próprio FileStream novo, para que cada iteração comece na posição zero.

Qual é a forma mais simples de converter um stream em um array de bytes em C#?

Um stream não informa o próprio tamanho de forma confiável, então convertê-lo em um array de bytes significa ler até os dados acabarem. Duas chamadas cobrem quase todos os casos.

Quando o stream permite posicionamento (seek) e informa um Length confiável, aloque um único array desse tamanho e preencha-o com ReadExactly(). O método repete a leitura internamente até encher o buffer e lança EndOfStreamException se o stream terminar antes, de modo que uma leitura incompleta vira um erro em vez de um array truncado sem aviso.

Quando o tamanho é desconhecido, o que é normal em streams de rede, de pipe e compactados, copie o stream para um MemoryStream com CopyTo() e chame ToArray() nele. Isso custa uma segunda cópia dos dados na memória e não exige um tamanho conhecido.

Os dois começam na posição atual do stream. Em um stream que já foi lido em parte, CopyTo() entrega apenas o restante e ReadExactly() lança uma exceção, porque o stream termina antes de o array ficar cheio; por isso, voltar ao início primeiro é responsabilidade de quem chama.

Vamos começar pelo caso em que o tamanho é conhecido:

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

    return bytes;
}

E, quando o tamanho é desconhecido, deixamos a leitura por conta de CopyTo():

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

    return memoryStream.ToArray();
}

Se o stream é um arquivo que nós mesmos abrimos, nem precisamos de um stream: File.ReadAllBytes() lê o arquivo inteiro em uma única chamada, e o artigo sobre como converter um arquivo em um array de bytes trata desse caso.

Como converter um stream com Stream.Read()?

Stream.Read() pode retornar menos bytes do que pedimos. O valor retornado é o número de bytes que o método de fato colocou no buffer, e esse número é uma promessa de progresso, não de conclusão. Um stream de arquivo pode entregar um bloco do disco, e um stream de rede entrega o que tiver chegado até o momento.

Por isso, uma única chamada nunca basta. O loop abaixo continua chamando Read() até que ele retorne zero, que é o único sinal de que o stream não tem mais dados para entregar.

O runtime faz isso por nós. ReadExactly() faz a mesma leitura repetida internamente e lança uma exceção quando o stream termina antes da hora, então o loop escrito à mão continua aqui como demonstração do que o framework faz agora, e não como código a ser copiado.

A parte cara não é o loop. É o uso de List<byte> e do buffer de 32 bytes, e é por isso que este método fica em último lugar no benchmark.

Um stream de doze bytes lido por quatro chamadas sucessivas a Read, que retornam cinco, cinco, dois e zero bytes.

Este é o mecanismo, não a recomendação. No .NET 7 e versões posteriores, use ReadExactly() quando o tamanho for conhecido.

Para garantir que vamos ler e converter corretamente todos os dados do stream com o método Stream.Read(), podemos criar um método que lê e copia os dados repetidamente até não restar mais nada para ler:

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;
}

Nesse método, começamos inicializando totalStream (um objeto List<byte>) e um array de bytes de tamanho 32 que serve de buffer.

Em seguida, definimos read, uma variável inteira que vai armazenar o número de bytes lidos do stream. Depois, usamos um loop while para ler repetidamente os dados do stream para o buffer até não restarem mais bytes para ler.

Na condição do loop while, verificamos o valor da variável read. Se ele for maior que zero, adicionamos os bytes lidos à lista totalStream com o método AddRange(), que os acrescenta ao final da lista. Usamos o método Take() para selecionar apenas a quantidade de bytes lidos e evitar adicionar à lista totalStream bytes antigos que sobraram no buffer.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Por fim, retornamos o conteúdo da lista totalStream como um array de bytes usando o método ToArray().

Como converter um stream com BinaryReader?

Uma forma mais fácil de converter um stream em um array de bytes em C# é usar a classe BinaryReader. Essa classe nos permite ler dados binários de um stream e convertê-los em um array de bytes. Ao contrário de Stream.Read(), BinaryReader.ReadBytes() continua lendo até obter o número de bytes que pedimos ou até o stream terminar, então uma única chamada basta.

Para ver como isso funciona, vamos criar um método UseBinaryReader():

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

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

    return bytes;
}

Aqui, instanciamos um objeto BinaryReader com o stream que queremos ler. Em seguida, usamos o método ReadBytes() da classe BinaryReader para ler os bytes do stream e convertê-los em um array de bytes.

O método ReadBytes() recebe um parâmetro inteiro que especifica o número de bytes a ler do stream. Neste caso, queremos ler todos os bytes do stream. Por isso, passamos o tamanho do stream como argumento.

Isso exige um stream que informe o próprio Length, e a conversão para int limita o tamanho a cerca de 2 GB, então este não é o método para um stream de rede ou um arquivo muito grande.

Por fim, retornamos o array de bytes com os dados do stream.

Como converter um stream com StreamReader?

Outra opção é converter um stream em um array de bytes com a classe StreamReader. Essa classe nos permite ler facilmente dados textuais de um stream, que depois convertemos em um array de bytes.

Para usar a classe StreamReader com esse objetivo, vamos definir um método UseStreamReader().

Este método está aqui para completar a lista e não deve ser usado para obter bytes. Ele decodifica o stream como texto e o codifica de novo, o que altera silenciosamente qualquer dado que não seja texto UTF-8 sem marca de ordem 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;
}

Primeiro, criamos uma instância da classe StreamReader e passamos o stream original como argumento. Depois, chamamos o método ReadToEnd() no leitor para ler todos os dados do stream como uma string.

Por fim, usamos o método GetBytes() da classe de codificação UTF8 para converter a string em um array de bytes e retorná-lo a quem chamou.

Este método supõe que o stream contém dados textuais codificados em UTF8. Se os dados estiverem em outra codificação, devemos passar a codificação correspondente ao construtor de StreamReader e usá-la no lugar de UTF8. Se o que temos na verdade é uma string, e não um stream, o caminho mais curto é converter uma string em um array de bytes.

Como converter um stream com MemoryStream?

Além disso, podemos usar a classe MemoryStream para converter streams em arrays de bytes. Essa classe lida com streams de tamanho desconhecido, ao custo de memória adicional durante a conversão, principalmente ao trabalhar com streams de dados grandes.

CopyTo() move os dados um bloco por vez, então o stream de origem nunca fica inteiro em um buffer alocado por nós. O próprio MemoryStream ainda cresce até comportar o payload inteiro, dobrando a capacidade a cada etapa, e ToArray() depois o copia de novo. É por isso que este método aloca mais do que um método que dimensiona o array de antemão.

Com isso em mente, vamos definir um 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;
}

Aqui, usamos uma instrução if para verificar se o stream que queremos converter já é um MemoryStream. Se for, chamamos o método ToArray() no stream e retornamos o array de bytes resultante.

Se o stream não for um MemoryStream, porém, criamos uma nova instância da classe MemoryStream. Depois, usamos o método CopyTo() para copiar o conteúdo do stream original para o stream em memória. Em seguida, chamamos o método ToArray() no stream em memória para obter a representação do stream como array de bytes.

Como converter um stream com BufferedStream?

Agora, vejamos outro método, que usa a classe BufferedStream.

A classe BufferedStream oferece uma forma de melhorar o desempenho de um stream com a adição de um buffer.

Usamos um buffer para armazenar dados temporariamente quando eles trafegam entre componentes de um sistema computacional que têm velocidades diferentes. Esse armazenamento momentâneo torna o processamento e a transferência de dados mais eficientes, porque diminui o número de chamadas entre os componentes do sistema. Ao converter um stream em um array de bytes com CopyTo(), porém, a cópia já lê em blocos grandes, então um buffer extra não reduz o tempo total de leitura do stream.

Para usar a classe BufferedStream na conversão de um stream em um array de bytes, vamos começar definindo um 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 de tudo, declaramos uma variável para guardar os dados que vamos converter. Depois, criamos uma instância da classe BufferedStream e passamos o stream original como argumento.

Em seguida, criamos um objeto MemoryStream para gravar os dados que lemos do objeto BufferedStream. Para transferir os dados entre os dois streams, usamos o método CopyTo() no objeto BufferedStream e passamos o objeto MemoryStream como argumento.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Essa transferência é necessária porque um BufferedStream não tem nenhum método que retorne o próprio conteúdo como array. Portanto, para converter o conteúdo do BufferedStream em um array byte[] sem saber o tamanho dele, precisamos primeiro copiar os dados para um buffer intermediário, como um MemoryStream, e depois convertê-lo em um array de bytes.

Por fim, chamamos o método ToArray() no objeto MemoryStream para obter os dados como um array de bytes e retorná-lo a quem chamou.

Qual é a forma mais rápida de converter um stream em um array de bytes em C#?

Agora que todos os métodos estão prontos, podemos avaliar o desempenho de cada um em tempo e em memória com um benchmark.

Agora podemos executar os testes de benchmark e examinar os resultados no console:

| 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 |

Os números vêm da biblioteca BenchmarkDotNet, e a implementação do benchmark está no nosso repositório.

Com base nos resultados do benchmark, vemos que UseReadExactly() é o método mais rápido para converter um stream em um array byte[] em C#, e é o que aloca menos memória: um array do tamanho do payload e nada mais.

UseBinaryReader() fica em segundo lugar, cerca de 4% atrás, e aloca o mesmo array único. Ele continua sendo o método indicado quando o código já lê valores binários tipados do stream, porque o leitor já está lá de qualquer forma.

Em seguida vêm UseBufferedStream(), UseCopyTo() e UseMemoryStream(), que formam um único grupo, e não três colocações: as margens de erro deles se sobrepõem, e os três alocam cerca de 2.639 KB, aproximadamente 3,7 vezes o que alocam os dois métodos acima deles. Esse excedente vem do MemoryStream, que dobra o próprio buffer até que ele comporte o payload, antes que ToArray() o copie de novo.

UseStreamReader() leva 934,8 us, cerca de 2,7 vezes o tempo do vencedor, e aloca 3.606 KB, o segundo maior valor da tabela. Esse é o custo de decodificar cada byte como texto e codificá-lo de novo.

Por fim, o método mais lento neste benchmark é UseStreamDotReadMethod(). Ele leva cerca de 4,5 vezes o tempo do vencedor e é o que mais aloca memória entre os sete. Esse é o preço de fazer List<byte> crescer de 32 em 32 bytes.

A velocidade, porém, é só um dos critérios, e o que costuma decidir a questão é se o stream conhece o próprio tamanho:

MétodoExige Length conhecidoUse quandoCuidado com
Stream.ReadExactly()SimO stream permite posicionamento e você quer uma única alocaçãoLança EndOfStreamException se o stream terminar antes da hora
Stream.CopyTo() para MemoryStreamNãoO tamanho é desconhecido: rede, pipe, dados compactadosMantém duas cópias do payload, uma no stream e outra no array
BinaryReader.ReadBytes()SimVocê já está lendo valores binários tipadosA conversão (int) limita o stream a cerca de 2 GB
StreamReader mais Encoding.GetBytes()NãoNunca, para obter bytesDecodifica como texto e codifica de novo, o que corrompe tudo o que não for texto UTF-8 sem BOM
BufferedStream envolvendo a origemNãoA origem faz muitas leituras pequenas sem bufferAdiciona um buffer sobre um CopyTo() que já tem buffer
File.ReadAllBytes()Não se aplicaA origem é um caminho de arquivo, não um streamNão é um método de stream: veja o artigo sobre como converter um arquivo em array de bytes

No geral, UseReadExactly() é a melhor opção para converter um stream em um array byte[] em C#, dado o baixo consumo de tempo e de memória. E, quando o objetivo do array é ser registrado em log, impresso ou colado em algum lugar, o próximo passo é transformar esses bytes em algo que uma pessoa consiga ler.

Como converter um array de bytes de volta em um stream em C#?

A conversão no sentido inverso é mais barata, porque um array de bytes já é, em si, o conteúdo que um stream entrega. Envolver um array em um MemoryStream não copia nada: o stream lê diretamente do array que passamos.

Dois construtores importam aqui. new MemoryStream(bytes) envolve o array em um stream de tamanho fixo, que não pode ser redimensionado, então ele não cresce além do array recebido, e gravar depois do fim falha. new MemoryStream() seguido de gravações cria um stream expansível com buffer próprio, que é o que queremos quando ainda há mais dados chegando.

Há um detalhe que pega as pessoas de surpresa. Um stream em que acabamos de gravar fica posicionado no fim dos dados, então quem ler o stream em seguida não vê nada. A solução é voltar Position para zero antes de repassar o stream, e esquecer isso é a causa mais comum de um resultado vazio.

Vamos envolver um array que já temos:

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

Como esse stream lê diretamente do nosso array, as gravações também vão no sentido inverso: gravar dentro dos limites dele altera diretamente o array de quem chamou, e gravar depois do fim lança NotSupportedException com a mensagem “Memory stream is not expandable.”.

Quando ainda há mais dados chegando, queremos a forma expansível:

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

    return stream;
}

Aqui, gravamos o array inteiro em um stream com buffer próprio e depois voltamos ao início. Gravar esse array de novo em outro lugar equivale a converter um array de bytes de volta em um arquivo.

A classe tem mais recursos do que esses dois construtores, e o artigo sobre como usar MemoryStream em C# explica o restante.

Conclusão

Neste artigo, discutimos sete métodos que podemos usar para converter um stream em um array de bytes em C#, e duas formas de fazer o caminho inverso. Além disso, executamos testes de benchmark para avaliar a velocidade e o uso de memória de todos os métodos, e ReadExactly() ficou em primeiro lugar nos dois quesitos.

Testado com .NET 10.0.10 e BenchmarkDotNet 0.15.8.