Manipulação de Arquivos em C#


Até agora, todos os dados que nossos programas utilizaram (como a posição das bombas no Campo Minado) eram perdidos quando o programa fechava. Para resolver isso, precisamos de um método de persistência de dados, ou seja, uma forma de salvar informações para que possam ser recuperadas depois. A maneira mais direta de fazer isso é através da manipulação de arquivos de texto. Neste capítulo, aprenderemos a ler e escrever em arquivos para salvar o layout do nosso jogo e o placar de vitórias e derrotas.

1. O Básico sobre Arquivos e Caminhos

Quando um programa precisa ler ou escrever em um arquivo, ele estabelece um "fluxo" de comunicação. Para evitar que outro programa (ou até o próprio usuário) modifique o arquivo ao mesmo tempo e cause erros, o arquivo fica "travado" durante o uso. Por isso, o procedimento padrão é sempre:

  1. Abrir o fluxo para o arquivo.
  2. Realizar as operações de leitura ou escrita.
  3. Fechar o fluxo para liberar o arquivo.

1.1 Localização do Arquivo e Caminhos

Para que nosso programa encontre o arquivo, precisamos dizer a ele onde procurar. Existem duas formas de fazer isso: usando um caminho absoluto ou relativo.

Primeiro, crie um arquivo chamado campo.txt e salve-o na pasta principal do seu projeto (no mesmo local do arquivo .sln), como mostra a figura:

Localização do arquivo campo.txt na pasta do projeto

Caminho Absoluto

É o endereço completo do arquivo, começando da raiz do seu HD (como C:\). É específico para cada computador. Ao usar em uma string em C#, precisamos duplicar as barras invertidas (\\) para que a linguagem não as confunda com caracteres de comando.

// Exemplo de caminho absoluto em uma string C#
string caminho_absoluto = "C:\\Users\\Rafael\\source\\repos\\Capitulo3\\campo.txt";

Caminho Relativo

É o endereço do arquivo tomando como ponto de partida o local onde o programa está sendo executado. No caso de um projeto C# no Visual Studio, o executável fica numa pasta como bin\Debug\netcoreappX.X. Para chegar ao nosso arquivo na raiz do projeto, precisamos "subir" alguns níveis de pasta. Usamos .. para subir um nível.

Localização da pasta de execução do projeto
// Exemplo de caminho relativo para subir 4 níveis e encontrar o arquivo
string caminho_relativo = "..\\..\\..\\..\\campo.txt";

2. Leitura de Dados de um Arquivo

Vamos alterar nosso Campo Minado para que ele carregue o mapa e o placar do arquivo campo.txt. O arquivo precisa ter um formato específico para que o programa consiga entendê-lo: 10 linhas para o campo (com números separados por vírgula), uma linha em branco, uma linha para vitórias e uma para derrotas.

Estrutura do arquivo de texto a ser lido

Código para Leitura e Explicação

O código a seguir abre o arquivo, lê os dados e monta o tabuleiro do jogo.

int[,] campo = new int[10, 10];
int[,] jogo = new int[10, 10];
int qtdLinhas = campo.GetLength(0);
int qtdColunas = campo.GetLength(1);
bool problemaArquivo = false;
string caminho_relativo = "..\\..\\..\\..\\campo.txt";

try
{
    // 1. Cria um leitor de fluxo para o arquivo especificado
    StreamReader sr = new StreamReader(caminho_relativo);
    
    string linha_arq = sr.ReadLine(); // 2. Lê a primeira linha do arquivo
    int linha_mtz = 0;

    // 3. Continua lendo linha por linha, até o fim do arquivo ou até ler as 10 linhas do campo
    while (linha_arq != null && linha_mtz < 10)
    {
        int coluna_mtz = 0;
        // 4. Separa a string da linha pela vírgula (ex: "0,0,1,0...") em várias strings menores ("0", "0", "1", "0"...)
        foreach (var numero in linha_arq.Split(','))
        {
            int num;
            // 5. Tenta converter a string para um número inteiro de forma segura
            if (int.TryParse(numero, out num))
            {
                campo[linha_mtz, coluna_mtz] = num; // Armazena na matriz do campo
                jogo[linha_mtz, coluna_mtz] = -1;  // Inicia a matriz do jogador
                coluna_mtz++;
            }
        }
        linha_arq = sr.ReadLine(); // Lê a próxima linha
        linha_mtz++;
    }

    // 6. Fecha o fluxo para liberar o arquivo
    sr.Close();
}
catch (Exception e)
{
    // 7. Se qualquer erro acontecer no bloco 'try', o código pula para cá
    Console.WriteLine("Ocorreu um problema na leitura do arquivo!");
    problemaArquivo = true;
}

Entendendo o Código

O restante do código do jogo agora é colocado dentro de uma condição para só ser executado se o arquivo for lido com sucesso.

if (!problemaArquivo)
{
    bool fimJogo = false;
    bool vitoria = false;
    do
    {
        // Imprime o tabuleiro do jogador a cada rodada
        for (int l = 0; l < qtdLinhas; l++)
        {
            for (int c = 0; c < qtdColunas; c++)
            {
                Console.Write(string.Format("{0} ", jogo[l, c]));
            }
            Console.Write(Environment.NewLine + Environment.NewLine);
        }

        // Pede a jogada para o usuário
        Console.Write("Selecione uma linha [1-10]: ");
        int linha = Convert.ToInt32(Console.ReadLine()) - 1;
        Console.Write("Selecione uma coluna [1-10]: ");
        int coluna = Convert.ToInt32(Console.ReadLine()) - 1;

        // Verifica o resultado da jogada
        switch (campo[linha, coluna])
        {
            case 0:
                jogo[linha, coluna] = 0;
                Console.Write("Continue tentando.\n\n");
                break;
            case 1:
                jogo[linha, coluna] = 1;
                Console.Write("BOOOM. Você perdeu.\n\n");
                fimJogo = true;
                break;
            default: // Caso encontre a bandeira (valor 2)
                jogo[linha, coluna] = 2;
                Console.Write("Parabéns. Você ganhou!\n\n");
                fimJogo = true;
                vitoria = true; // Atualiza a flag de vitória
                break;
        }
    } while (!fimJogo);
}

3. Escrita de Dados em um Arquivo

Após o fim de cada partida, vamos atualizar o placar de vitórias ou derrotas no arquivo. A estratégia será: ler todo o conteúdo do arquivo para a memória, modificar apenas a linha que queremos e, em seguida, reescrever o arquivo inteiro com o conteúdo atualizado.

Código para Escrita e Explicação

Este código deve ser inserido dentro do if (!problemaArquivo), logo após o final do loop do-while do jogo.

try
{
    // 1. Lê todas as linhas do arquivo e guarda em um vetor de strings
    string[] arquivo = File.ReadAllLines(caminho_relativo);
    
    // Pega as linhas de vitória e derrota (penúltima e última)
    string msgVitorias = arquivo[arquivo.Length - 2];
    string msgDerrotas = arquivo[arquivo.Length - 1];

    // 2. Abre um fluxo de ESCRITA. IMPORTANTE: Isso apaga o conteúdo atual do arquivo!
    StreamWriter sw = new StreamWriter(caminho_relativo);

    int contagem;
    int linha_sobrescrever;
    string texto;

    if (vitoria)
    {
        // Pega o número depois de "Vitórias:"
        int.TryParse(msgVitorias.Split(':')[1], out contagem);
        linha_sobrescrever = 11; // A contagem de vitórias está na 12ª linha (índice 11)
        texto = "Vitórias:";
    }
    else
    {
        // Pega o número depois de "Derrotas:"
        int.TryParse(msgDerrotas.Split(':')[1], out contagem);
        linha_sobrescrever = 12; // A contagem de derrotas está na 13ª linha (índice 12)
        texto = "Derrotas:";
    }

    contagem++; // 3. Incrementa o placar

    // 4. Percorre o vetor que continha as linhas originais
    for (int i = 0; i < arquivo.Length; i++)
    {
        // Se for a linha que queremos mudar...
        if (i == linha_sobrescrever)
        {
            sw.WriteLine(texto + contagem); // Escreve a nova linha com o placar atualizado
        }
        else
        {
            sw.WriteLine(arquivo[i]); // Senão, apenas reescreve a linha original
        }
    }
    
    // 5. Fecha o fluxo para salvar as alterações no arquivo
    sw.Close();
}
catch (Exception e)
{
    Console.WriteLine("Ocorreu um problema na escrita do arquivo!");
}

Entendendo o Código

Veja o seguinte conteúdo para melhor entendimento: link


Considerações Finais

A manipulação de arquivos é um recurso poderoso e relativamente simples para dar "memória" aos seus programas. Com as classes StreamReader e StreamWriter, e o uso correto de tratamento de exceções com try-catch, você pode salvar e carregar configurações, placares, progresso de jogo e muito mais, tornando suas aplicações mais robustas e dinâmicas.