CodeGym /Cursos /C# SELF /Trabalhando com fluxos de bytes

Trabalhando com fluxos de bytes

C# SELF
Nível 36 , Lição 4
Disponível

1. Introdução

A gente já conhece fluxos (Stream), que são tipo uma abstração pra ler ou gravar dados de forma sequencial. Já usamos FileStream pra acessar arquivos no nível de bytes, e também StreamReader e StreamWriter pra trabalhar de boa com dados de texto, que por baixo dos panos usam FileStream e cuidam da codificação.

Mas e se a gente quiser guardar no arquivo não só texto, mas dados fortemente tipados: números inteiros (int), números de ponto flutuante (double, float), valores booleanos (bool), datas (DateTime) ou até structs customizadas? Dá pra converter tudo pra string e gravar com StreamWriter, depois fazer o parse na leitura. Só que esse jeito tem uns perrengues:

  • Ineficiente pra armazenar: O número 12345 gravado como texto ocupa 5 bytes (caracteres). No formato binário, um int ocupa só 4 bytes. Pra grandes volumes de dados, essa diferença pesa.
  • Performance: Ficar convertendo número pra string e depois de volta é gasto de CPU à toa.
  • Precisão dos dados: Converter número quebrado pra texto e depois de volta pode perder precisão por causa de arredondamento.
  • Parsing complicado: Fazer parse manual de strings pra extrair vários tipos de dados (tipo "123,45 TRUE 2024-06-21") deixa o código mais difícil e frágil.

Pra resolver isso, existem as classes BinaryReader e BinaryWriter. Elas são adaptadores que trabalham por cima de qualquer Stream (geralmente FileStream) e trazem métodos práticos pra ler e gravar tipos primitivos do C# no formato binário. Elas cuidam de converter bytes pra tipos e vice-versa, deixando tudo mais fácil pra trabalhar com arquivos binários estruturados.

A ideia chave: BinaryReader e BinaryWriter não são fluxos independentes. Eles turbinam um Stream já existente, adicionando métodos pra trabalhar com tipos do C# em vez de só bytes crus.

2. Gravando dados com BinaryWriter

BinaryWriter tem vários métodos Write(), sobrecarregados pra cada tipo primitivo do C#. Quando tu chama um desses métodos, o BinaryWriter converte o valor pro formato binário (sequência de bytes) e grava esses bytes no Stream base.

Exemplo: Salvando configurações do jogo

Imagina que a gente quer salvar as configs do jogo: volume (float), nível atual do jogador (int), se a música tá ligada (bool) e o nível de dificuldade escolhido (string).


string filePath = "settings.bin";

// 1. Cria o FileStream pra gravar
using FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
// 2. Cria o BinaryWriter por cima do FileStream, define a codificação pra strings (se for usar)
using BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8);
// 3. Grava vários tipos de dados
writer.Write(0.75f);       // float (4 bytes)
writer.Write(15);          // int (4 bytes)
writer.Write(true);        // bool (1 byte)
writer.Write("Easy");      // string (prefixo de tamanho + bytes)
                
Console.WriteLine($"Configurações salvas em '{filePath}'.");

Explicando o exemplo:

  • A gente cria um FileStream com FileMode.Create, que cria um arquivo novo ou sobrescreve se já existir.
  • Depois cria o BinaryWriter, passando o fs. Importante: o BinaryWriter por padrão fecha o fluxo base (fs) quando tu chama Dispose() (que é chamado automaticamente pelo bloco using).
  • Os métodos writer.Write() são bem intuitivos: Write(float), Write(int), Write(bool), Write(string). Eles já sabem quantos bytes gravar pra cada tipo e como representar.
  • Pra strings, o BinaryWriter automaticamente coloca um prefixo de tamanho antes dos bytes da string. Isso faz o BinaryReader saber exatamente quantos bytes ler pra reconstruir a string.
  • Se tu tentar abrir o settings.bin num editor de texto, vai ver "lixo", porque é arquivo binário. Pra ver o conteúdo, só com editor HEX.

3. Lendo dados com BinaryReader

BinaryReader tem métodos ReadXxx() (tipo ReadInt32(), ReadBoolean(), ReadString()), que lêem a quantidade certa de bytes do Stream base e convertem pro tipo C# que tu pediu.

Exemplo: Carregando configurações do jogo

Agora vamos ler as configs do arquivo settings.bin que a gente criou antes.


string filePath = "settings.bin";

// 1. Cria o FileStream pra leitura
using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
// 2. Cria o BinaryReader por cima do FileStream, usa a mesma codificação
using BinaryReader reader = new BinaryReader(fs, Encoding.UTF8);

// 3. Lê os dados NA MESMA ORDEM em que foram gravados
float volume = reader.ReadSingle();     // float
int level = reader.ReadInt32();         // int
bool isMusicOn = reader.ReadBoolean();  // bool
string difficulty = reader.ReadString(); // string
                
Console.WriteLine($"Configurações carregadas de '{filePath}':");
Console.WriteLine($"- Volume: {volume:P0}"); // Formata como porcentagem (usando string format)
Console.WriteLine($"- Nível do jogador: {level}");
Console.WriteLine($"- Música ligada: {isMusicOn}");
Console.WriteLine($"- Dificuldade: {difficulty}");

Explicando o exemplo:

  • Abre o FileStream em FileMode.Open pra leitura.
  • Cria o BinaryReader por cima do fs, usando a mesma codificação que usou pra gravar.
  • Muito importante: A ordem dos métodos reader.ReadXxx() tem que ser EXATAMENTE a mesma da gravação com BinaryWriter. Se tu tentar ler uma string onde foi gravado um int, vai dar erro EndOfStreamException (se a string for maior) ou vai ler dados errados.
  • Os métodos ReadXxx() já lêem a quantidade certa de bytes e convertem pro tipo pedido. ReadString() usa aquele prefixo de tamanho gravado pelo BinaryWriter pra saber quantos bytes ler pra string toda.

4. Detalhes importantes e melhores práticas

Ordem estrita:

Essa é a regra principal. BinaryReader e BinaryWriter não guardam metadados de tipos; eles só sabem quantos bytes cada tipo primitivo ocupa. Tu tem que garantir que a ordem bate certinho.

Gerenciamento de recursos (using):

Como a maioria das classes do .NET que mexem com recursos do sistema (tipo arquivos ou conexões de rede), tanto BinaryReader quanto BinaryWriter implementam IDisposable. Então sempre usa eles dentro de um bloco using — assim tu garante que o Dispose() vai ser chamado automaticamente, mesmo se der erro. Isso evita vazamento de recurso e fecha o arquivo certinho.

Aliás, por padrão o BinaryWriter e o BinaryReader também chamam Dispose() pro fluxo base que tu passou (tipo FileStream), então ele também vai ser fechado automático.


using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);
// ... trabalho

Codificação pra strings:

Pra strings funcionarem de boa, sempre passa a mesma codificação no construtor do BinaryWriter.Write(string) e do BinaryReader.ReadString() (tipo Encoding.UTF8). Senão pode dar ruim com caracteres fora do ASCII.

Tratamento de exceções:

Operações de leitura/gravação de arquivo podem ser interrompidas por fatores externos (arquivo não existe, sem permissão, disco cheio). Sempre coloca o código com FileStream e BinaryReader/BinaryWriter dentro de um bloco try-catch pra garantir robustez.

BaseStream e posição:

Tu pode acessar o fluxo base pela propriedade BaseStream (tipo reader.BaseStream ou writer.BaseStream). Isso é útil se tu quiser saber a posição atual (BaseStream.Position) ou pular pra outro lugar no arquivo (BaseStream.Seek()).


// Exemplo de uso do BaseStream.Position
using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);

writer.Write(123);
Console.WriteLine($"Posição atual no fluxo: {writer.BaseStream.Position}"); // Vai mostrar 4 (tamanho do int)

writer.Write("Hello");
Console.WriteLine($"Posição atual no fluxo: {writer.BaseStream.Position}"); // Vai mostrar 4 + (1+5) = 10

⚠️ O método Write(string) primeiro grava o tamanho da string como um inteiro de 7 bits, depois os bytes da string. Então o tamanho final nem sempre é 1 + tamanho da string.

1
Pesquisa/teste
Fluxos de entrada e saída, nível 36, lição 4
Indisponível
Fluxos de entrada e saída
Leitura e escrita de arquivos
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION