C# -  Usando streams : FileStream


  Hoje vamos recordar o conceito de streams e mostrar o uso da classe FileStream.

Não tem ideia do que é um stream e nunca ouviu falar na classe FileStream. Então acompanhe...

Um stream é um objeto que representa uma sequência de bytes, permitindo que o programador leia ou escreva dados em uma fonte de dados, como um arquivo, um socket de rede ou uma memória. Ele fornece uma interface padronizada para operações de entrada e saída de dados, tornando mais fácil trabalhar com diferentes tipos de fontes de dados.

Resumindo : um stream é um fluxo de dados (bytes) que se move de um ponto a outro, assim como um fluxo de água fluindo em um rio e são usados para ler e gravar dados de e para :  arquivos,  redes,  memória e outros streams.

As classes usadas para trabalhar com streams são parte do namespace System.IO e incluem:

Essas classes fornecem uma interface padronizada para trabalhar com streams de diferentes fontes de dados e permitem que os programadores realizem operações de E/S de maneira mais eficiente e flexível.

Vamos focar neste artigo na classe FileStream que implementa a classe Stream e nas classes auxiliares StreamReader e StreamWriter:

Como ler um arquivo usando o FileStream

A seguir temos um exemplo que vai ler o conteúdo do arquivo informado pelo usuário usando o método ReadFile:
 

using System.Text;
Console.WriteLine("Informe o caminho e nome do arquivo texto : ");
string? caminhoArquivo = Console.ReadLine();
try
{
    ReadFile(caminhoArquivo);
}
catch (IOException ex)
{
    Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}
Console.ReadKey();
static void ReadFile(string path) //ex: @"d:\dados\poesia.txt"
{
    //abre o arquivo para leitura
    var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
    //define array de bytes com o tamanho do arquivo
    var buffer = new byte[fileStream.Length];
    //le o arquivo
    fileStream.Read(buffer);
    //decodifica o array de bytes em uma string
    var resultado = Encoding.UTF8.GetString(buffer);
    Console.WriteLine($"\n{resultado}");
    // certifique-se de que todos os dados ainda no buffer
    // (bytesArray) sejam gravados no arquivo
    fileStream.Flush();
    //liberar quaisquer recursos do sistema associados ao objeto
    fileStream.Close(); 
}
O código var buffer = new byte[fileStream.Length]; cria um array de bytes chamado buffer com o tamanho igual ao comprimento do arquivo representado pelo objeto fileStream.

O array de bytes é usado para armazenar temporariamente o conteúdo do arquivo, que será lido ou gravado usando as classes de fluxo (stream) do C#. O tamanho do array é definido com base no tamanho do arquivo para garantir que haja espaço suficiente para armazenar todo o conteúdo do arquivo.

Uma forma mais simplificada de escrever o método acima é usar o bloco using.

O bloco using atua da seguinte forma : quando a variável que você declarou dentro do using sai do escopo, ela chama o método .Dispose() que chama internamente .Flush() e .Close() automaticamente para nós. Assim não corremos o risco de esquecer de liberar os recursos usados.

static void ReadFileUsing(string path)
{
    using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
    {
        var bytesArray = new byte[fileStream.Length];
        fileStream.Read(bytesArray);
        var resultado = Encoding.UTF8.GetString(bytesArray);
        Console.WriteLine($"\n{resultado}");
    }
}

Podemos simplificar o uso do bloco using usando a declaração using (C# 8) :

static void ReadFileUsing(string path)
{
    using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);

    var bytesArray = new byte[fileStream.Length];
    fileStream.Read(bytesArray);
    var resultado = Encoding.UTF8.GetString(bytesArray);
    Console.WriteLine($"\n{resultado}");
}

Nos exemplos acima, nem precisamos usar o FileStream. O StreamReader lida com o FileStream sob o capô para tornar nossas vidas mais simples.

E se precisarmos ler um arquivo maior do que a nossa memória RAM disponível ?

Uma coisa é certa, não podemos ler todo o arquivo na memória de uma só vez! Temos que usar a abordagem de partes por partes do arquivo.

static string ReadLargeFile(string path)
{
    // le o arquivo em pedaços de 4KB
    const int bufferSize = 4096; 
    var builder = new StringBuilder();
    using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
    using (var streamReader = new StreamReader(fileStream))
    {
        char[] buffer = new char[bufferSize];
        int bytesRead;
        while ((bytesRead = streamReader.ReadBlock(buffer, 0, bufferSize)) > 0)
        {
            builder.Append(buffer, 0, bytesRead);
        }
    }
    return builder.ToString();
}
Este código lê um arquivo de texto e retorna seu conteúdo como uma string. Ele lê o arquivo em blocos de tamanho fixo (4KB), em vez de lê-lo inteiramente de uma só vez, o que pode ser útil para lidar com arquivos grandes.

Aqui está o que cada linha do código faz:

  • const int bufferSize = 4096;
     define o tamanho do buffer usado para ler o arquivo. Neste caso, é 4KB.
  • var builder = new StringBuilder();
    cria um objeto StringBuilder, que é usado para construir a string final que será retornada
  • using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read)):
    abre um FileStream para o arquivo especificado no caminho path com permissões somente de leitura.
  • using (var streamReader = new StreamReader(fileStream))
    cria um objeto StreamReader para ler o conteúdo do FileStream.
  • char[] buffer = new char[bufferSize];
    cria um array de caracteres do tamanho do buffer.
  • int bytesRead;
    cria uma variável para armazenar o número de bytes lidos em cada leitura.
  • while ((bytesRead = streamReader.ReadBlock(buffer, 0, bufferSize)) > 0)
    lê o conteúdo do arquivo em blocos de tamanho fixo e armazena o número de caracteres lidos em bytesRead. O loop continua enquanto ainda houver caracteres a serem lidos.
  • builder.Append(buffer, 0, bytesRead);
    adiciona os caracteres lidos ao objeto StringBuilder.
  • return builder.ToString();
    converte o objeto StringBuilder em uma string e retorna o resultado.

Como gravar em um arquivo usando o FileStream

A seguir temos um método que recebe dois argumentos : o caminho do arquivo e o texto a ser escrito no arquivo:

static void WriteText(string path, string texto) //E.x. @"d:\dados\teste.txt"
{
    try
    {
        byte[] bytesArray = Encoding.UTF8.GetBytes(texto);
        using (var fileStream = new FileStream(path, FileMode.Create))
        {
            fileStream.Write(bytesArray);
        }
    }
    catch (Exception)
    {
        throw;
    }
}

Podemos simplificar o código usando apenas a classe StreamWriter :

static void WriteText(string path, string texto) 
{
    try
    {
        using (var s = new StreamWriter(path))
        {
            s.WriteLine(texto);
        }
    }
    catch (Exception)
    {
        throw;
    }
}

Nota: A classe File também fornece alguns métodos úteis. O exemplo acima poderia ter sido escrito como: File.WriteAllText(path, texto);

Para gravar no arquivo podemos usar o seguinte código na classe Program:

using System.Text;
Console.WriteLine("Informe o caminho e nome do arquivo texto : ");
string? caminhoArquivo = Console.ReadLine();
Console.WriteLine("Informe o texto a ser gravado ");
string? texto = Console.ReadLine();
try
{
    WriteText(caminhoArquivo, texto);
    Console.WriteLine("Texto gravado com sucesso !!!");
}
catch (IOException ex)
{
    Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}
Console.ReadKey();

Vamos levantar o mesmo problema de tamanho novamente. E se você quiser escrever um arquivo grande demais para caber na sua RAM de uma só vez ?

Podemos usar a mesma abordagem que usamos para ler o arquivo grande:

static void WriteFileLarge(string path, string content)
{
    // Cria um novo arquivo 
    using (FileStream stream = new FileStream(path, FileMode.Create, FileAccess.Write))
    {
        // Escreve em pedaços de 1024 bytes
        byte[] buffer = Encoding.UTF8.GetBytes(content);
        int chunkSize = 1024;
        for (int i = 0; i < buffer.Length; i += chunkSize)
        {
            int remainingBytes = buffer.Length - i;
            int bytesToWrite = remainingBytes < chunkSize ? remainingBytes : chunkSize;
            stream.Write(buffer, i, bytesToWrite);
        }
    }
}

Com isso temos alguns exemplos de como ler e gravar em arquivos textos usando FileStream e StreamReader e StreamWriter.

Pegue o exemplo aqui : AppStreams.zip 

"Louvai ao SENHOR. Louvai ao SENHOR desde os céus, louvai-o nas alturas.
Louvai-o, todos os seus anjos; louvai-o, todos os seus exércitos.
Louvai-o, sol e lua; louvai-o, todas as estrelas luzentes."
Salmos 148:1-3

Referências:


José Carlos Macoratti