C# Ler e Escrever
Na aula Arquivos, cada método da classe File fazia tudo de uma vez: abria, lia (ou gravava) o arquivo inteiro e fechava. Às vezes você precisa de mais controle: gravar uma linha agora e outra daqui a pouco, ler um arquivo enorme sem carregá-lo todo, ou ler um arquivo e escrever outro ao mesmo tempo. Para isso existem o StreamWriter ("escritor de fluxo") e o StreamReader ("leitor de fluxo"). Nesta aula você vai usá-los no diário e na lista de alunos da Ana, e vai entender por que um arquivo pode ficar vazio quando você esquece de fechá-lo.
- Gravar aos poucos com
StreamWriter:WriteLineeWrite. - Ler linha por linha com
StreamReader.ReadLinee o laçowhile ((linha = …) != null). - Por que o
usingé obrigatório, e a diferença entre a declaraçãousinge o blocousing (…) { }. - O que é o buffer, e o que fazem o
Flushe oDispose. - Acrescentar no fim (
append: true), ler tudo comReadToEnde escolher a codificação. - Ler um arquivo e gravar outro ao mesmo tempo.
Esta aula usa using e IDisposable, while e referências anuláveis (string?). Os programas rodam com dotnet run no .NET 10 da máquina da Ana, na pasta ~/cs.
A ideia: o balde e a mangueira
O File.ReadAllText é um balde: ele pega toda a água de uma vez e entrega o balde cheio. Funciona bem para pouca água. Para uma piscina, o balde não aguenta.
Um stream ("fluxo", "correnteza") é uma mangueira: a água passa aos poucos, e você usa cada pedaço quando ele chega. Você abre a torneira (cria o leitor ou o escritor), usa a água pelo tempo que quiser e, no fim, fecha a torneira. Esquecer de fechar é o erro mais comum desta aula.
| Classe | Tradução | Para que serve | Métodos principais |
|---|---|---|---|
StreamWriter | escritor de fluxo | Gravar texto num arquivo, aos poucos | WriteLine, Write, Flush |
StreamReader | leitor de fluxo | Ler texto de um arquivo, aos poucos | ReadLine, ReadToEnd, EndOfStream |
Os dois ficam em System.IO, que já vem incluído. Você já conhece o jeito de usá-los: o WriteLine e o Write do StreamWriter funcionam como os do Console, e o ReadLine do StreamReader funciona como o Console.ReadLine. A diferença é que, em vez da tela e do teclado, o texto vai para um arquivo e vem de um arquivo.
1. Gravar aos poucos: StreamWriter
A Ana escreve um diário de estudos. Cada linha é gravada com um comando:
using StreamWriter escritor = new StreamWriter("diario.txt");
escritor.WriteLine("Segunda: estudei C#");
escritor.Write("Terça: ");
escritor.WriteLine("fiz a lista de compras");
escritor.WriteLine($"Total de dias: {2}");
Console.WriteLine("Diário gravado");
- new StreamWriter("diario.txt")
- Abre o arquivo para escrita. Como no
File.WriteAllText, se o arquivo não existe ele é criado, e se existe é esvaziado logo na abertura. - using StreamWriter escritor = …;
- A declaração
usingC# 8: "quando a variávelescritorsair de cena, feche o arquivo". Aqui, ela sai de cena no fim do programa. A aula using e IDisposable explica o mecanismo; a seção 3 mostra por que ele é obrigatório. - escritor.WriteLine("…")
- Grava o texto e uma quebra de linha, como o
Console.WriteLine. - escritor.Write("Terça: ")
- Grava o texto sem quebra de linha. O próximo
WriteLinecontinua na mesma linha. - $"Total de dias: {2}"
- A interpolação funciona igual: qualquer
stringserve.
$ dotnet run diario.cs Diário gravado $ cat diario.txt Segunda: estudei C# Terça: fiz a lista de compras Total de dias: 2
Repare que nada do diário apareceu na tela: só o Console.WriteLine escreve na tela. O escritor.WriteLine escreve no arquivo.
2. Ler linha por linha: StreamReader
using StreamReader leitor = new StreamReader("diario.txt");
string? linha;
int n = 0;
while ((linha = leitor.ReadLine()) != null)
{
n++;
Console.WriteLine($"{n}: {linha}");
}
Console.WriteLine($"Fim: {n} linhas");
- using StreamReader leitor = new StreamReader("diario.txt");
- Abre o arquivo para leitura, com a posição no começo. Se ele não existe, lança
FileNotFoundException(aula Arquivos). - string? linha;
- O
?diz que a variável pode guardarnull(aula Referências Anuláveis). Ela precisa poder, porque é assim que o leitor avisa o fim. - leitor.ReadLine()
- Lê a próxima linha, sem a quebra, e avança. Quando não há mais linhas, devolve
null. - while ((linha = leitor.ReadLine()) != null)
- A forma clássica, lida de dentro para fora: lê uma linha, guarda em
linhae compara o resultado comnull. Os parênteses em volta da atribuição são obrigatórios. Se não for o fim, executa o corpo e volta para ler a próxima.
$ dotnet run ler.cs 1: Segunda: estudei C# 2: Terça: fiz a lista de compras 3: Total de dias: 2 Fim: 3 linhas
Em cada volta do laço, só uma linha está na memória. Com um arquivo de 2 GB, este programa funciona igual, e gasta a mesma memória.
Outras formas de ler
| Código | O que faz |
|---|---|
leitor.ReadLine() | A próxima linha, ou null no fim |
leitor.ReadToEnd() | Tudo o que falta, numa string (como o File.ReadAllText) |
leitor.EndOfStream | true quando não há mais nada para ler |
leitor.Read() | Um caractere, como int; -1 no fim |
Com o EndOfStream ("fim do fluxo"), o laço também pode ser escrito assim:
using StreamReader leitor = new StreamReader("diario.txt");
while (!leitor.EndOfStream)
{
Console.WriteLine(leitor.ReadLine());
}
$ dotnet run fim.cs Segunda: estudei C# Terça: fiz a lista de compras Total de dias: 2
As duas formas funcionam. A do != null é a mais usada, porque já deixa a linha na variável.
3. O buffer, o Flush e o fechar
Ir ao disco é lento, comparado com a memória. Por isso o StreamWriter não grava cada WriteLine no disco na hora: ele junta o texto numa área de memória, o buffer ("reservatório"), e só manda para o disco quando o buffer enche, quando você pede (Flush, "descarga") ou quando o arquivo é fechado (Dispose). O tamanho do arquivo mostra isso:
using StreamWriter escritor = new StreamWriter("parcial.txt");
escritor.WriteLine("primeira linha");
Console.WriteLine($"Antes do Flush: {new FileInfo("parcial.txt").Length} bytes");
escritor.Flush();
Console.WriteLine($"Depois do Flush: {new FileInfo("parcial.txt").Length} bytes");
$ dotnet run flush.cs Antes do Flush: 0 bytes Depois do Flush: 15 bytes
- new FileInfo("parcial.txt").Length
- O tamanho do arquivo no disco, em bytes (a classe
FileInfoaparece na aula Pastas e Caminhos). - Antes do Flush: 0 bytes
- O
WriteLinejá foi executado, mas a linha ainda está no buffer. - escritor.Flush();
- Manda o que está no buffer para o disco:
primeira linha(14 letras) mais a quebra de linha = 15 bytes.
O Dispose, que o using chama sozinho, faz o Flush e depois fecha o arquivo. Você quase nunca precisa chamar o Flush: basta fechar o arquivo na hora certa. E a hora certa importa. Veja este programa, que grava o diário e lê de volta logo depois:
using StreamWriter escritor = new StreamWriter("diario.txt");
escritor.WriteLine("Segunda: estudei C#");
escritor.WriteLine("Terça: fiz a lista de compras");
escritor.WriteLine("Quarta: li um livro");
using StreamReader leitor = new StreamReader("diario.txt");
string? linha;
int n = 0;
while ((linha = leitor.ReadLine()) != null)
{
n++;
Console.WriteLine($"{n}: {linha}");
}
Console.WriteLine($"Fim: {n} linhas");
$ dotnet run diario2.cs Fim: 0 linhas $ cat diario.txt « depois que o programa terminou, as linhas estão lá Segunda: estudei C# Terça: fiz a lista de compras Quarta: li um livro
A declaração using só fecha o escritor no fim do programa. Quando o leitor abriu o arquivo, as três linhas ainda estavam no buffer, e o arquivo estava vazio. Para fechar o escritor antes, use o bloco using: o arquivo é fechado na chave }.
using (StreamWriter escritor = new StreamWriter("diario.txt"))
{
escritor.WriteLine("Segunda: estudei C#");
escritor.WriteLine("Terça: fiz a lista de compras");
escritor.WriteLine("Quarta: li um livro");
} // aqui o escritor é fechado
| Forma | Fecha quando… | Use quando… |
|---|---|---|
using StreamWriter e = new(…); | termina o bloco em que a variável está (no programa de cima, o fim do programa) | O arquivo fica aberto até o fim mesmo |
using (StreamWriter e = new(…)) { … } | chega a chave } do bloco | Você vai usar o arquivo de novo depois (ler o que gravou, abrir outra vez) |
Missão: rode o programa e veja o Fim: 0 linhas. Troque a declaração using do escritor por um bloco using (StreamWriter escritor = new StreamWriter("diario.txt")) { … } com os três WriteLine dentro, para o arquivo ser fechado antes da leitura. A saída deve terminar com 3: Quarta: li um livro e Fim: 3 linhas.
using StreamWriter escritor = new StreamWriter("diario.txt");
escritor.WriteLine("Segunda: estudei C#");
escritor.WriteLine("Terça: fiz a lista de compras");
escritor.WriteLine("Quarta: li um livro");
using StreamReader leitor = new StreamReader("diario.txt");
string? linha;
int n = 0;
while ((linha = leitor.ReadLine()) != null)
{
n++;
Console.WriteLine($"{n}: {linha}");
}
Console.WriteLine($"Fim: {n} linhas");
using (StreamWriter escritor = new StreamWriter("diario.txt"))
{
escritor.WriteLine("Segunda: estudei C#");
escritor.WriteLine("Terça: fiz a lista de compras");
escritor.WriteLine("Quarta: li um livro");
}
using StreamReader leitor = new StreamReader("diario.txt");
string? linha;
int n = 0;
while ((linha = leitor.ReadLine()) != null)
{
n++;
Console.WriteLine($"{n}: {linha}");
}
Console.WriteLine($"Fim: {n} linhas");
4. Acrescentar, atalhos e codificação
O StreamWriter esvazia o arquivo ao abrir. Para gravar no fim, como num log, passe append: true ("acrescentar"). A classe File também tem três atalhos que criam leitores e escritores:
using (StreamWriter log = new StreamWriter("log.txt", append: true))
{
log.WriteLine("Programa iniciado");
}
using (StreamWriter log = File.AppendText("log.txt"))
{
log.WriteLine("Venda: caderno");
}
using (StreamReader leitor = File.OpenText("log.txt"))
{
Console.Write(leitor.ReadToEnd());
}
- new StreamWriter("log.txt", append: true)
- Abre para escrita com a posição no fim: o que já existe fica. Se o arquivo não existe, é criado.
- File.AppendText("log.txt")
- Atalho para o mesmo
StreamWritercomappend: true. - File.OpenText("log.txt")
- Atalho para
new StreamReader("log.txt"). Existe também oFile.CreateText, atalho paranew StreamWriter(…)semappend. - leitor.ReadToEnd()
- Lê tudo o que falta de uma vez.
$ dotnet run log.cs Programa iniciado Venda: caderno $ dotnet run log.cs Programa iniciado Venda: caderno Programa iniciado Venda: caderno
Outra codificação
Como os métodos da classe File, o leitor e o escritor usam UTF-8 se você não disser nada (aula Arquivos). Para um arquivo em Latin-1, passe a codificação no construtor:
using System.Text;
using (StreamWriter w = new StreamWriter("antigo.txt", false, Encoding.Latin1))
{
w.WriteLine("São Paulo");
}
Console.WriteLine($"{new FileInfo("antigo.txt").Length} bytes");
using StreamReader r = new StreamReader("antigo.txt", Encoding.Latin1);
Console.WriteLine(r.ReadLine());
$ dotnet run latin.cs 10 bytes São Paulo $ file antigo.txt antigo.txt: ISO-8859 text
O segundo argumento do escritor, false, é o append. Em Latin-1, ã ocupa 1 byte: São Paulo (9) + quebra de linha (1) = 10 bytes. Em UTF-8 seriam 11.
5. Ler um arquivo e escrever outro
É aqui que os streams brilham. A Ana tem as notas da turma em alunos.csv e quer um arquivo aprovados.csv só com quem tirou 7 ou mais. O programa lê uma linha, decide e grava, sem carregar nenhum dos dois arquivos inteiros:
$ cat alunos.csv nome;nota Ana;9,5 Bruno;6,5 Carla;8 Diego;5 Elisa;10
using StreamReader leitor = new StreamReader("alunos.csv");
using StreamWriter escritor = new StreamWriter("aprovados.csv");
string? cabecalho = leitor.ReadLine();
escritor.WriteLine(cabecalho);
int aprovados = 0;
string? linha;
while ((linha = leitor.ReadLine()) != null)
{
string[] campos = linha.Split(';');
double nota = double.Parse(campos[1]);
if (nota >= 7)
{
escritor.WriteLine(linha);
aprovados++;
}
}
Console.WriteLine($"{aprovados} aprovados gravados em aprovados.csv");
- dois using seguidos
- Um leitor e um escritor abertos ao mesmo tempo, em arquivos diferentes. No fim do programa, os dois são fechados, na ordem inversa da abertura (primeiro o escritor).
- leitor.ReadLine() antes do laço
- Lê só a primeira linha, o cabeçalho
nome;nota, e o copia para o arquivo novo. O laço começa da segunda linha. - double.Parse(campos[1])
- As notas usam vírgula, como no Brasil, e o computador da Ana está em português: o
Parseentende9,5. Se o arquivo usasse ponto, seria preciso oCultureInfo.InvariantCulture(aula Cultura). - escritor.WriteLine(linha)
- Copia a linha original, sem mudar nada, para o arquivo de aprovados.
$ dotnet run aprovados.cs 3 aprovados gravados em aprovados.csv $ cat aprovados.csv nome;nota Ana;9,5 Carla;8 Elisa;10
File ou Stream: qual usar
| Você quer… | Use |
|---|---|
| Ler ou gravar um arquivo pequeno de uma vez | File.ReadAllText, File.WriteAllText e companhia (aula Arquivos) |
| Passar pelas linhas de um arquivo grande | File.ReadLines ou StreamReader.ReadLine |
| Gravar muitas linhas, aos poucos, num laço | StreamWriter (um File.AppendAllText por linha abriria e fecharia o arquivo a cada volta) |
| Ler um arquivo e gravar outro ao mesmo tempo | StreamReader + StreamWriter |
| Ler um caractere por vez | StreamReader.Read() |
| Ler sem travar o programa | ReadLineAsync, WriteLineAsync (aula async e await) |
Quando dá errado
using
StreamWriter escritor = new StreamWriter("esqueci.txt");
escritor.WriteLine("linha importante");
Console.WriteLine("Gravei?");
$ dotnet run esqueci.cs Gravei? $ ls -l esqueci.txt -rw-rw-r-- 1 ana ana 0 out 17 12:43 esqueci.txt $ cat esqueci.txt $
O que aconteceu: o arquivo foi criado (o new cria), mas tem 0 bytes. A linha ficou no buffer, e ninguém chamou o Dispose nem o Flush. Quando o programa terminou, o buffer sumiu junto com a memória. Não há erro nem aviso: o programa diz "Gravei?" e o dado se perdeu.
Como resolver: todo StreamWriter nasce com using: using StreamWriter escritor = new StreamWriter("esqueci.txt");. O using fecha o arquivo até se o programa lançar uma exceção no meio.
CS8600 e CS8602: string sem ?
using StreamReader leitor = new StreamReader("diario.txt");
string linha = leitor.ReadLine();
Console.WriteLine(linha.ToUpper());
$ dotnet run aviso.cs /home/ana/cs/aviso.cs(2,16): warning CS8600: Conversão de literal nula ou possível valor nulo em tipo não anulável. /home/ana/cs/aviso.cs(3,19): warning CS8602: Desreferência de uma referência possivelmente nula. SEGUNDA: ESTUDEI C#
O que aconteceu: o programa rodou, mas o compilador avisou duas vezes. O ReadLine devolve string?, porque pode devolver null no fim do arquivo, e a variável foi declarada como string, que "nunca é null" (CS8600). Por isso o linha.ToUpper() pode falhar com NullReferenceException se o arquivo estiver vazio (CS8602). Em inglês: Converting null literal or possible null value to non-nullable type.
Como resolver: declare string? linha e confira o null antes de usar: no laço while ((linha = leitor.ReadLine()) != null), ou com if (linha != null). Veja a aula Avisos do Compilador.
ObjectDisposedException: ler depois de fechar
StreamReader leitor = new StreamReader("diario.txt");
leitor.Dispose();
Console.WriteLine(leitor.ReadLine());
$ dotnet run fechado.cs; echo "código: $?" Unhandled exception. System.ObjectDisposedException: Cannot read from a closed TextReader. Object name: 'StreamReader'. at System.IO.StreamReader.<ThrowIfDisposed>g__ThrowObjectDisposedException|73_0() at System.IO.StreamReader.ReadLine() at Program.<Main>$(String[] args) in /home/ana/cs/fechado.cs:line 3 código: 134
O que aconteceu: "Não é possível ler de um TextReader fechado". Depois do Dispose, o leitor não serve mais. Com o bloco using (…) { }, isso acontece quando você usa a variável fora das chaves (mas aí o compilador já reclama: a variável nem existe fora do bloco, erro CS0103).
Como resolver: faça todas as leituras antes de fechar. Para ler de novo, abra um leitor novo.
using StreamWriter a = new StreamWriter("dois.txt");
using StreamWriter b = new StreamWriter("dois.txt");
a.WriteLine("de a");
b.WriteLine("de b");
Console.WriteLine("ok");
$ dotnet run dois.cs ok $ cat dois.txt de a
O que aconteceu: no Linux, os dois escritores abriram o mesmo arquivo, cada um com o seu buffer e a sua posição no começo. No fim, o b foi fechado primeiro e gravou de b no começo do arquivo; depois o a fechou e gravou de a por cima. Nenhum erro. (No Windows, o segundo new StreamWriter lançaria IOException, dizendo que o arquivo está sendo usado por outro processo.)
Como resolver: um arquivo, um escritor. Se duas partes do programa precisam gravar no mesmo arquivo, passe o mesmo StreamWriter para as duas.
Referência rápida
| Código | O que faz |
|---|---|
using StreamWriter e = new StreamWriter(c); | Abre para gravar (cria ou esvazia); fecha no fim do bloco |
new StreamWriter(c, append: true) / File.AppendText(c) | Abre para gravar no fim |
File.CreateText(c) | Atalho para new StreamWriter(c) |
e.WriteLine(texto) / e.Write(texto) | Grava com / sem quebra de linha |
e.Flush() | Manda o buffer para o disco agora |
using StreamReader l = new StreamReader(c); / File.OpenText(c) | Abre para ler |
while ((linha = l.ReadLine()) != null) | Lê linha por linha até o fim (string? linha) |
l.ReadToEnd() | O resto do arquivo numa string |
l.EndOfStream | true no fim |
new StreamReader(c, Encoding.Latin1) | Lê com outra codificação (using System.Text;) |
using (…) { … } | Fecha na chave } |
Exercícios
1. O fim do arquivo
O que o ReadLine() de um StreamReader devolve quando não há mais linhas?
2. Gravar no fim
Complete para abrir o log sem apagar o que já está nele:
3. A palavra que fecha
Complete para o arquivo ser fechado sozinho no fim do bloco:
4. Um programa cria um StreamWriter sem using, grava 3 linhas e termina. O que tem no arquivo?
5. Por que a variável do laço de leitura é string?, e não string?
6. O programa grava um arquivo com using StreamWriter e = …; (declaração) e, nas linhas seguintes, lê o mesmo arquivo. A leitura acha o arquivo vazio. Qual é o conserto?
Missão final: os aprovados
Missão: o programa copia o alunos.csv (na outra aba) inteiro para aprovados.csv e depois mostra o arquivo novo. Mude o laço para gravar só os alunos com nota 7 ou mais: separe a linha com Split(';'), converta a nota com double.Parse e grave a linha só se ela passar. A saída deve ser exatamente:
3 aprovados nome;nota Ana;9,5 Carla;8 Elisa;10
using (StreamReader leitor = new StreamReader("alunos.csv"))
using (StreamWriter escritor = new StreamWriter("aprovados.csv"))
{
escritor.WriteLine(leitor.ReadLine());
int aprovados = 0;
string? linha;
while ((linha = leitor.ReadLine()) != null)
{
escritor.WriteLine(linha);
aprovados++;
}
Console.WriteLine($"{aprovados} aprovados");
}
Console.Write(File.ReadAllText("aprovados.csv"));
nome;nota Ana;9,5 Bruno;6,5 Carla;8 Diego;5 Elisa;10
using (StreamReader leitor = new StreamReader("alunos.csv"))
using (StreamWriter escritor = new StreamWriter("aprovados.csv"))
{
escritor.WriteLine(leitor.ReadLine());
int aprovados = 0;
string? linha;
while ((linha = leitor.ReadLine()) != null)
{
string[] campos = linha.Split(';');
double nota = double.Parse(campos[1]);
if (nota >= 7)
{
escritor.WriteLine(linha);
aprovados++;
}
}
Console.WriteLine($"{aprovados} aprovados");
}
Console.Write(File.ReadAllText("aprovados.csv"));
Os dois using (…) seguidos, com um bloco só, abrem os dois arquivos e fecham os dois na mesma chave }. Por isso o File.ReadAllText do fim já encontra o aprovados.csv completo. Se quiser ir além: grave também um reprovados.csv com os outros, usando um segundo escritor.
Resumo
StreamWritergrava aos poucos (WriteLine,Write);StreamReaderlê aos poucos (ReadLine,ReadToEnd).- O laço de leitura é
while ((linha = leitor.ReadLine()) != null), comstring? linha: onullmarca o fim do arquivo. - O escritor guarda o texto num buffer e só grava no disco no
Flushou noDispose. Semusing, o arquivo pode ficar vazio. - A declaração
usingfecha no fim do bloco em que está; o blocousing (…) { }fecha na chave}. Feche o escritor antes de ler o mesmo arquivo. append: true(ouFile.AppendText) grava no fim; a codificação é UTF-8, a não ser que você passe outra.- Um leitor e um escritor juntos processam um arquivo inteiro gastando a memória de uma linha.