Manipulação de arquivos no C#

A manipulação de arquivos em C# é uma tarefa comum em boa parte dos sistemas, e o .NET oferece a classe File para criar, copiar, substituir, ler e escrever arquivos de forma simples e direta. Neste artigo, você vai conhecer cada operação, os erros mais comuns e as boas práticas para trabalhar com o disco sem surpresas.

O que é a manipulação de arquivos em C#

O .NET é um conjunto de bibliotecas que auxilia na criação de aplicações. Como você deve imaginar, ler e gravar dados no disco faz parte da rotina de quase todo software.

Logs, relatórios, arquivos de configuração e exportações em CSV são exemplos clássicos. Por isso, a manipulação de arquivos em C# aparece cedo na carreira de quem trabalha com a plataforma.

Na prática, manipular um arquivo significa criar, abrir, escrever, ler, copiar, mover, substituir e excluir esse arquivo. Além disso, envolve consultar informações como datas e atributos.

O .NET concentra essas operações no namespace System.IO. Assim, você escreve o mesmo código e ele funciona em Windows, Linux e macOS, com raras exceções.

A classe File e o namespace System.IO

A classe System.IO.File é estática e oferece métodos para criar, copiar, excluir, mover e abrir um único arquivo. Ela também ajuda a criar objetos FileStream, que representam o fluxo de bytes do arquivo.

Segundo a documentação oficial da classe File, todos os métodos exigem o caminho do arquivo que você deseja manipular. Esse caminho pode ser absoluto, relativo ou até um caminho UNC de rede.

Quando você vai repetir várias operações sobre o mesmo arquivo, a classe FileInfo pode ser mais eficiente. Isso acontece porque ela guarda o caminho em uma instância e evita verificações repetidas.

Para facilitar os exemplos a seguir, vamos começar com uma constante que define o caminho do arquivo. Como o exemplo roda no Windows, o caminho fica assim:

const string filePath = @"C:\dev\myfile.txt";

O @ antes da string cria um literal verbatim. Dessa forma, você não precisa escapar as barras invertidas do caminho.

Em projetos multiplataforma, prefira montar o caminho com Path.Combine. Esse método usa o separador correto de cada sistema operacional.

// Monta o caminho com o separador correto de cada sistema
var filePath = Path.Combine(AppContext.BaseDirectory, "data", "myfile.txt");

Tratamento de erros na manipulação de arquivos em C#

A manipulação de arquivos em C# é uma operação sensível. O disco pode estar cheio, a pasta pode não existir, outro processo pode estar usando o arquivo ou o usuário pode não ter permissão.

Por isso, a recomendação é sempre envolver essas operações em um bloco try/catch, conforme o exemplo abaixo:

try
{
    // Manipulação de arquivo
}
catch (Exception ex)
{
    // Capturar exceção
    Console.WriteLine(ex.Message);
}

Capturar Exception funciona, mas tratar as exceções específicas deixa o código mais claro. Assim, você reage de forma diferente para cada tipo de falha.

try
{
    File.Copy(filePath, @"C:\dev\other.txt");
}
catch (FileNotFoundException)
{
    // O arquivo de origem não existe
    Console.WriteLine("Source file not found.");
}
catch (DirectoryNotFoundException)
{
    // A pasta informada no caminho não existe
    Console.WriteLine("Directory not found.");
}
catch (UnauthorizedAccessException)
{
    // Falta de permissão ou arquivo somente leitura
    Console.WriteLine("Access denied.");
}
catch (IOException ex)
{
    // Erro genérico de entrada e saída, como arquivo em uso
    Console.WriteLine(ex.Message);
}

Repare na ordem dos blocos catch. Como FileNotFoundException e DirectoryNotFoundException herdam de IOException, elas precisam vir antes da classe base.

Já a UnauthorizedAccessException não herda de IOException. Portanto, ela precisa de um bloco próprio.

Operações básicas de manipulação de arquivos em C#

Com o caminho definido e o tratamento de erros em mente, podemos conhecer os métodos mais usados na manipulação de arquivos em C#. Cada um resolve uma etapa do ciclo de vida de um arquivo.

Criar um arquivo

O primeiro método é o File.Create, que cria ou sobrescreve um arquivo no disco. Se o arquivo não existe, o método cria um novo; se já existe, ele sobrescreve o conteúdo.

É importante destacar que o caminho das pastas precisa existir. Caso contrário, você recebe uma DirectoryNotFoundException, porque o método não cria a pasta automaticamente.

Além disso, se não houver permissão de escrita no diretório informado, o .NET lança uma UnauthorizedAccessException.

// File.Create devolve um FileStream, então feche o recurso
using var stream = File.Create(filePath);

Um detalhe que muita gente esquece: File.Create devolve um FileStream aberto. Se você não fechar esse objeto, o arquivo continua bloqueado e a próxima operação sobre ele falha.

Quando a pasta pode não existir, crie a estrutura antes com Directory.CreateDirectory. Esse método não gera erro se a pasta já estiver lá.

// Garante que a pasta existe antes de criar o arquivo
Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
using var stream = File.Create(filePath);

Adicionar conteúdo a um arquivo

Para escrever em um arquivo, podemos usar o AppendText ou o CreateText. A diferença é que o AppendText adiciona texto ao fim do arquivo, enquanto o CreateText sobrescreve o conteúdo atual.

using (var appendWriter = File.AppendText(filePath))
{
    appendWriter.WriteLine("Test"); // Adiciona o texto ao fim do arquivo
}

using (var createWriter = File.CreateText(filePath))
{
    createWriter.WriteLine("Test 2"); // Substitui o conteúdo do arquivo com este texto
}

Os dois métodos devolvem um StreamWriter. Com o using, o .NET fecha o arquivo ao fim do bloco, então você não precisa chamar Close manualmente.

Ao trabalhar com arquivos grandes, você não precisa ler o arquivo inteiro para adicionar conteúdo. Nesse caso, o AppendText é a escolha certa.

// ERRADO: lê o arquivo inteiro só para acrescentar uma linha
var data = File.ReadAllText(filePath);
data += "My additional text";
using (var writer = File.CreateText(filePath))
{
    writer.WriteLine(data); // Substitui o conteúdo do arquivo com este texto
}

// CORRETO: escreve apenas no fim do arquivo
using (var appendWriter = File.AppendText(filePath))
{
    appendWriter.WriteLine("My additional text"); // Adiciona o texto ao fim do arquivo
}

Para operações rápidas, a classe também oferece File.WriteAllText e File.AppendAllText. Esses métodos abrem, escrevem e fecham o arquivo em uma única chamada.

File.WriteAllText(filePath, "First line\n");   // Cria ou sobrescreve
File.AppendAllText(filePath, "Second line\n"); // Acrescenta ao fim

De acordo com a documentação, o AppendAllText cria o arquivo se ele não existir. No entanto, ele também não cria pastas, então o diretório precisa existir.

Copiar um arquivo

Na manipulação de arquivos em C#, a cópia fica a cargo do File.Copy, que recebe o caminho do arquivo atual e o destino. O caminho de destino precisa apontar para um arquivo, nunca para uma pasta.

File.Copy(filePath, @"C:\dev\other.txt");

Se já houver outro arquivo com o mesmo nome no destino, você recebe uma IOException. Para sobrescrever o arquivo de destino, use o parâmetro adicional overwrite.

File.Copy(filePath, @"C:\dev\other.txt", overwrite: true); // Sobrescreve o destino se precisar

Vale saber também que a cópia mantém os atributos do arquivo original. Por exemplo, um arquivo somente leitura gera uma cópia também somente leitura.

Mover um arquivo

Da mesma forma, o File.Move move um arquivo de um diretório para outro. Ele também serve para renomear, já que renomear nada mais é do que mover para um novo nome na mesma pasta.

File.Move(filePath, @"C:\dev\temp\new.txt", overwrite: true);

Os cuidados são os mesmos do método anterior. A pasta de destino precisa existir e, sem o parâmetro overwrite, um arquivo com o mesmo nome no destino gera erro.

Substituir um arquivo

O File.Replace substitui o conteúdo de um arquivo pelo conteúdo de outro. Em seguida, ele exclui o arquivo de origem e cria um backup do arquivo substituído.

// Origem, destino que será substituído e backup do destino
File.Replace(@"C:\dev\other.txt", filePath, @"C:\dev\backup.txt");

No exemplo, o conteúdo de other.txt vai para myfile.txt, e a versão antiga de myfile.txt vira backup.txt. Se você não quiser backup, passe null no terceiro parâmetro.

Segundo a documentação, origem e destino precisam estar no mesmo volume. Além disso, os dois arquivos precisam existir, ou o método lança uma FileNotFoundException.

Excluir um arquivo

Para excluir um arquivo, a manipulação de arquivos em C# conta com o método File.Delete, que recebe o caminho completo. O método não aceita curingas como *.txt.

File.Delete(filePath);

Um ponto que costuma surpreender: se o arquivo não existir, o File.Delete não lança exceção. Por outro lado, ele falha se o arquivo estiver em uso, se for somente leitura ou se a pasta do caminho não existir.

Verificar se um arquivo existe

Para verificar se um arquivo existe, usamos o File.Exists com o caminho completo. O método devolve true ou false.

var exists = File.Exists(filePath); // Retorna um booleano

Entretanto, o File.Exists não substitui o tratamento de erros. Entre a verificação e a operação seguinte, outro processo pode excluir ou bloquear o arquivo.

Exemplo completo de manipulação de arquivos em C#

Para fixar os conceitos, vale juntar as operações em um único fluxo. O exemplo abaixo mostra uma manipulação de arquivos em C# do início ao fim, com tratamento de erros.

var folder = Path.Combine(AppContext.BaseDirectory, "reports");
var reportPath = Path.Combine(folder, "report.txt");
var archivePath = Path.Combine(folder, "archive", "report-old.txt");

try
{
    // Cria as pastas necessárias
    Directory.CreateDirectory(Path.GetDirectoryName(archivePath)!);

    // Escreve o conteúdo inicial e acrescenta uma linha
    File.WriteAllText(reportPath, "Daily report\n");
    File.AppendAllText(reportPath, $"Generated at {DateTime.Now}\n");

    // Faz uma cópia de segurança e move o original para o arquivo morto
    File.Copy(reportPath, reportPath + ".bak", overwrite: true);
    File.Move(reportPath, archivePath, overwrite: true);

    Console.WriteLine(File.Exists(archivePath)); // True
}
catch (IOException ex)
{
    Console.WriteLine($"I/O error: {ex.Message}");
}
catch (UnauthorizedAccessException ex)
{
    Console.WriteLine($"Access denied: {ex.Message}");
}

Primeiro, o código garante que a estrutura de pastas existe. Em seguida, ele grava o relatório, cria um backup e move o arquivo original para a pasta de arquivo morto.

Repare que nenhum stream fica aberto ao fim do fluxo. Isso acontece porque WriteAllText e AppendAllText abrem e fecham o arquivo sozinhos.

Na prática, esse padrão aparece em rotinas de exportação, geração de logs e processamento em lote. Basta trocar os nomes e os conteúdos para adaptar o fluxo ao seu cenário.

Abertura e leitura de arquivos

A leitura merece um conteúdo próprio, porque existem várias abordagens com vantagens diferentes. Para saber mais, confira o artigo como ler arquivos em C#.

De forma resumida, File.ReadAllText e File.ReadAllLines carregam o arquivo inteiro na memória. Já File.ReadLines e StreamReader leem linha a linha, o que funciona melhor para arquivos grandes.

// Lê linha a linha, sem carregar o arquivo inteiro na memória
foreach (var line in File.ReadLines(filePath))
{
    Console.WriteLine(line);
}

Se o objetivo é gerar planilhas simples, o artigo sobre gerar CSV com C# mostra como aplicar esses métodos na prática.

Arquivos grandes e streams na manipulação de arquivos em C#

Para arquivos grandes, a recomendação na manipulação de arquivos em C# é usar o StreamWriter para escrita e o StreamReader para leitura. Essas classes processam o conteúdo em partes, sem colocar tudo na memória.

// Escrita em lote com StreamWriter, acrescentando ao fim do arquivo
using (var writer = new StreamWriter(filePath, append: true))
{
    for (var i = 0; i < 10_000; i++)
    {
        writer.WriteLine($"Line {i}");
    }
}

// Leitura linha a linha com StreamReader
using (var reader = new StreamReader(filePath))
{
    string? line;
    while ((line = reader.ReadLine()) is not null)
    {
        Console.WriteLine(line);
    }
}

O mesmo vale para aplicações web e serviços que atendem muitas requisições. Nesses cenários, os métodos assíncronos como File.ReadAllTextAsync e WriteLineAsync liberam a thread enquanto o disco trabalha.

await File.WriteAllTextAsync(filePath, "Async content");
var content = await File.ReadAllTextAsync(filePath);

Datas e atributos na manipulação de arquivos em C#

Além das operações de conteúdo, a manipulação de arquivos em C# inclui consultar metadados. A classe File oferece métodos para datas e atributos de cada arquivo.

Obter a data de acesso e de modificação

Um método bem útil é o File.GetLastAccessTime, que devolve a data e a hora do último acesso a um arquivo. O valor vem em um DateTime no horário local.

var lastAccess = File.GetLastAccessTime(filePath); // DateTime
Console.WriteLine(lastAccess);

Contudo, a data de último acesso não é a mesma coisa que a data de última modificação. Para saber quando alguém escreveu no arquivo pela última vez, use o File.GetLastWriteTime.

var lastWrite = File.GetLastWriteTime(filePath);       // Última escrita, horário local
var lastWriteUtc = File.GetLastWriteTimeUtc(filePath); // Última escrita, em UTC
var created = File.GetCreationTime(filePath);          // Data de criação

A documentação avisa que esses valores podem ser imprecisos, porque o sistema operacional nem sempre os atualiza na hora. Se o arquivo não existir, o método devolve 1º de janeiro de 1601, ajustado para o horário local.

Para entender melhor o tipo devolvido, vale ler o artigo sobre DateTime no C# e .NET.

Obter atributos do arquivo

Para os demais atributos, usamos o File.GetAttributes, que devolve um valor do enumerador FileAttributes. Esse enumerador funciona como um conjunto de flags, ou seja, um arquivo pode ter vários atributos ao mesmo tempo.

var attributes = File.GetAttributes(filePath);

if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
    Console.WriteLine("read-only");

if ((attributes & FileAttributes.Compressed) == FileAttributes.Compressed)
    Console.WriteLine("compressed");

O operador & isola cada flag dentro do valor. Também é possível usar o método HasFlag, que deixa a leitura mais simples.

if (attributes.HasFlag(FileAttributes.Hidden))
    Console.WriteLine("hidden");

Métodos específicos de plataforma na manipulação de arquivos em C#

A maior parte da manipulação de arquivos em C# funciona em qualquer plataforma que suporte o .NET. No entanto, alguns métodos rodam exclusivamente no Windows.

Um exemplo é o File.Encrypt, que depende do sistema de criptografia de arquivos do NTFS. Em Linux, macOS ou em edições Home do Windows, ele lança PlatformNotSupportedException ou NotSupportedException.

O compilador e a IDE alertam sempre que você usa um método específico de plataforma. Isso acontece por causa do analisador CA1416, que verifica chamadas a APIs marcadas com o atributo SupportedOSPlatform.

// Só chama a API quando o código roda no Windows
if (OperatingSystem.IsWindows())
{
    File.Encrypt(filePath);
}

Com essa verificação, o aviso desaparece e o código continua seguro em outros sistemas operacionais.

Boas práticas de manipulação de arquivos em C#

Depois de conhecer os métodos, vale reunir algumas boas práticas de manipulação de arquivos em C#. Elas evitam a maioria dos erros que aparecem em produção.

Primeiro, sempre feche os recursos com using. Um FileStream ou StreamWriter aberto mantém o arquivo bloqueado e impede outras operações.

Em seguida, monte caminhos com Path.Combine em vez de concatenar strings. Assim, o código funciona em qualquer sistema operacional.

Também trate exceções específicas, como IOException e UnauthorizedAccessException. Dessa forma, você dá mensagens claras ao usuário e registra o erro correto.

Para arquivos grandes, prefira streams e leitura linha a linha. Por fim, em aplicações web, use as versões assíncronas dos métodos.

Se o destino dos arquivos for a nuvem, o artigo sobre upload de imagem para o Azure Storage com C# mostra o próximo passo.

Conclusão

A classe File oferece um enorme leque de opções, e a manipulação de arquivos em C# é simples e direta. Com poucos métodos, você cria, escreve, copia, move, substitui e exclui arquivos.

O principal cuidado está no tratamento de erros, já que leitura e escrita dependem do disco, das permissões e de outros processos. Portanto, combine try/catch, using e caminhos bem montados para ter um código confiável.

Com essas práticas, a manipulação de arquivos em C# deixa de ser uma fonte de bugs e passa a ser uma ferramenta confiável no dia a dia. Agora, experimente aplicar os exemplos em um projeto de console e observe o comportamento de cada método.

FAQ

Qual a diferença entre File e FileInfo no C#?

A classe File é estática e ideal para operações pontuais. Já a FileInfo guarda o caminho em uma instância e funciona melhor quando você repete várias operações sobre o mesmo arquivo.

O File.Create cria as pastas do caminho?

Não. Se a pasta não existir, o método lança DirectoryNotFoundException, então crie a estrutura antes com Directory.CreateDirectory.

O File.Delete gera erro se o arquivo não existir?

Não. O método só lança exceção quando o arquivo está em uso, é somente leitura, a pasta não existe ou falta permissão.

Como renomear um arquivo em C#?

Use o File.Move, informando o caminho atual e o novo nome na mesma pasta. Para sobrescrever um arquivo existente, passe overwrite: true.

Quando usar StreamWriter em vez de File.WriteAllText?

Use o StreamWriter quando for escrever muito conteúdo em partes ou em arquivos grandes. Para gravar um texto curto de uma vez, o File.WriteAllText é mais prático.