Projeto Lista de Tarefas
A Ana anota o que tem de fazer em papeizinhos colados na geladeira: "comprar pão", "estudar C#", "pagar a conta de luz". Os papéis caem, se perdem, e ela nunca sabe quantos ainda faltam. Neste projeto, ela escreve um programa de lista de tarefas: anota, marca como feita, apaga, e a lista continua lá no dia seguinte. É o primeiro projeto do guia com várias classes em vários arquivos, do jeito que os programas de verdade são organizados.
- Escrever uma classe com propriedades, construtor e métodos, num arquivo próprio.
- Proteger os dados com
private seteprivate(Encapsulamento) e validar no construtor comthrow. - Sobrescrever o
ToString(aula A Classe object). - Usar uma
List<T>por dentro de outra classe:Add,Find,RemoveAll. - Fazer um menu com
switcheint.TryParse. - Guardar a lista num arquivo de texto com
StreamWriter,usingeFile.ReadAllLines.
O que vamos construir
O programa pronto, num dia comum. Ontem, a Ana anotou três tarefas e fez uma. Hoje, ela abre o programa, vê a lista, anota mais uma, conclui outra, limpa as feitas e sai:
$ dotnet run === Lista de Tarefas === 3 tarefas no caderno, 2 por fazer. 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 1 [ ] 1. Comprar pão [x] 2. Estudar C# [ ] 3. Pagar a conta de luz 2 de 3 por fazer. 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Título: Ligar para a vó Anotado: [ ] 4. Ligar para a vó 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 3 Número da tarefa: 1 Feito: [x] 1. Comprar pão 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 5 Apaguei 2 tarefas feitas. 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 1 [ ] 3. Pagar a conta de luz [ ] 4. Ligar para a vó 2 de 2 por fazer. 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 0 Até logo! Por fazer: 2.
- A classe Tarefa (propriedades, construtor,
ToString). - A classe ListaDeTarefas e o menu (adicionar e concluir).
- Remover e limpar as feitas (
RemoveAll, e o número que não é a posição). - A lista guardada em tarefas.txt (
StreamWritereusing).
Antes de começar
Você precisa ter visto Classes e Objetos, Propriedades, Construtores e List<T>. Com várias classes, cada uma no seu arquivo, o programa precisa de um projeto (aula Projetos): o dotnet compila juntos todos os .cs da pasta.
$ cd projetos $ dotnet new console -o Tarefas O modelo "Aplicativo do Console" foi criado com êxito. Processando ações pós-criação... Restaurando /home/ana/projetos/Tarefas/Tarefas.csproj: Determinando os projetos a serem restaurados... /home/ana/projetos/Tarefas/Tarefas.csproj restaurado (em 108 ms). A restauração foi bem-sucedida. $ cd Tarefas
No fim, a pasta terá três arquivos de código e o arquivo da lista:
| Arquivo | O que tem | Desde |
|---|---|---|
Tarefa.cs | A classe Tarefa: uma tarefa só. | versão 1 |
ListaDeTarefas.cs | A classe ListaDeTarefas: todas as tarefas, e as regras de adicionar, concluir, apagar e salvar. | versão 2 |
Program.cs | O menu: conversa com a Ana e chama a lista. | versão 1 |
tarefas.txt | A lista guardada, que o próprio programa cria. | versão 4 |
Versão 1 · A classe Tarefa
Um papelzinho da geladeira tem três coisas: um número (para a Ana dizer "fiz a 2"), o texto, e um risco quando a tarefa está feita. A classe Tarefa é o molde desse papel. Ela fica no arquivo Tarefa.cs:
// Tarefa.cs — uma tarefa da lista
public class Tarefa
{
public int Id { get; }
public string Titulo { get; }
public bool Feita { get; private set; }
public Tarefa(int id, string titulo)
{
if (string.IsNullOrWhiteSpace(titulo))
{
throw new ArgumentException("A tarefa precisa de um título.", nameof(titulo));
}
Id = id;
Titulo = titulo.Trim();
}
public void Concluir()
{
Feita = true;
}
public override string ToString() => $"[{(Feita ? "x" : " ")}] {Id}. {Titulo}";
}
| Linha | O que faz |
|---|---|
public class Tarefa | Declara a classe. public: qualquer parte do programa pode usá-la. |
public int Id { get; } | Uma propriedade só de leitura: o número da tarefa. Só o construtor pode dar valor a ela; depois, ninguém muda (aula Propriedades). |
public string Titulo { get; } | O texto da tarefa, também só de leitura. |
public bool Feita { get; private set; } | Todo mundo pode ler se está feita, mas só a própria classe pode mudar (o set é private). Começa com false, o valor padrão do bool. |
public Tarefa(int id, string titulo) | O construtor: roda no new Tarefa(…) e preenche o papel. |
if (string.IsNullOrWhiteSpace(titulo)) | Recusa título vazio ou só com espaços. Uma tarefa sem texto não serve para nada, e é melhor recusar na porta do que descobrir depois. |
throw new ArgumentException("…", nameof(titulo)); | Lança uma exceção: "argumento inválido". O nameof(titulo) vira o texto "titulo", o nome do parâmetro culpado (aula throw). |
Titulo = titulo.Trim(); | Guarda o título sem os espaços do começo e do fim. |
public void Concluir() | O único jeito de marcar como feita. Quem está fora da classe pede "conclua", em vez de mexer no Feita direto. |
public override string ToString() => … | Diz como a tarefa vira texto. É ele que o Console.WriteLine(t) usa. Sem ele, sairia só o nome da classe, Tarefa (aula A Classe object). |
$"[{(Feita ? "x" : " ")}] {Id}. {Titulo}" | Monta [x] 2. Estudar C# ou [ ] 1. Comprar pão. O ternário fica entre parênteses porque, dentro de { }, o : sozinho seria lido como início de um formato. |
O Program.cs cria três tarefas, guarda numa lista, conclui uma e mostra:
// Program.cs — versão 1: a classe Tarefa e uma List
List<Tarefa> tarefas = [];
tarefas.Add(new Tarefa(1, "Comprar pão"));
tarefas.Add(new Tarefa(2, "Estudar C#"));
tarefas.Add(new Tarefa(3, "Pagar a conta de luz"));
tarefas[1].Concluir(); // a posição 1 é a segunda tarefa
foreach (Tarefa t in tarefas)
{
Console.WriteLine(t);
}
int pendentes = tarefas.Count(t => !t.Feita);
Console.WriteLine($"{pendentes} de {tarefas.Count} por fazer.");
$ ls obj Program.cs Tarefa.cs Tarefas.csproj $ dotnet run [ ] 1. Comprar pão [x] 2. Estudar C# [ ] 3. Pagar a conta de luz 2 de 3 por fazer.
O dotnet run compilou os dois arquivos juntos: o Program.cs usa a classe Tarefa sem nenhum using ou #include, porque os dois estão no mesmo projeto. Repare também no tarefas[1]: a posição 1 é a segunda tarefa, porque as listas contam a partir do 0. Guarde isso: vai dar problema na versão 3.
set é privado
A Ana tentou marcar a tarefa como feita "na mão", pelo Program.cs:
List<Tarefa> tarefas = []; tarefas.Add(new Tarefa(1, "Comprar pão")); tarefas[0].Feita = true; Console.WriteLine(tarefas[0]);
$ dotnet run /home/ana/projetos/Tarefas/Program.cs(3,1): error CS0272: A propriedade ou o indexador "Tarefa.Feita" não pode ser usado neste contexto porque o acessador set é inacessível [/home/ana/projetos/Tarefas/Tarefas.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: o Feita tem private set; fora da classe, ele pode ser lido, mas não mudado. Em inglês: The property or indexer 'Tarefa.Feita' cannot be used in this context because the set accessor is inaccessible.
Como consertar: peça para a tarefa se concluir: tarefas[0].Concluir();. Esse é o motivo de existir o private set: se amanhã concluir uma tarefa também tiver de guardar a data, a mudança é feita num lugar só, o Concluir.
List<Tarefa> tarefas = []; tarefas.Add(new Tarefa(1, "Comprar pão")); tarefas.Add(new Tarefa(2, " ")); Console.WriteLine(tarefas.Count);
$ dotnet run Unhandled exception. System.ArgumentException: A tarefa precisa de um título. (Parameter 'titulo') at Tarefa..ctor(Int32 id, String titulo) in /home/ana/projetos/Tarefas/Tarefa.cs:line 12 at Program.<Main>$(String[] args) in /home/ana/projetos/Tarefas/Program.cs:line 3 $ echo $? 134
O que significa: o construtor recusou o título " ". A mensagem é a que a Ana escreveu, com o nome do parâmetro em inglês no fim ((Parameter 'titulo')). O rastro mostra o caminho: Tarefa..ctor (o construtor, "constructor"), na linha 12 de Tarefa.cs, chamado pela linha 3 de Program.cs.
Como consertar: aqui, a exceção é o comportamento certo: a classe se protegeu. Quem chama é que deve conferir antes. Na versão 2, o menu confere o título digitado e avisa a Ana sem derrubar o programa.
Missão (versão 1): nesta caixa, o método Concluir da aba Tarefa.cs está vazio, e nenhuma tarefa fica marcada. Escreva a linha que falta (uma só) e rode: a segunda tarefa deve sair [x] 2. Estudar C#.
// Program.cs — versão 1: a classe Tarefa e uma List
List<Tarefa> tarefas = [];
tarefas.Add(new Tarefa(1, "Comprar pão"));
tarefas.Add(new Tarefa(2, "Estudar C#"));
tarefas.Add(new Tarefa(3, "Pagar a conta de luz"));
tarefas[1].Concluir(); // a posição 1 é a segunda tarefa
foreach (Tarefa t in tarefas)
{
Console.WriteLine(t);
}
int pendentes = tarefas.Count(t => !t.Feita);
Console.WriteLine($"{pendentes} de {tarefas.Count} por fazer.");
// Tarefa.cs — uma tarefa da lista
public class Tarefa
{
public int Id { get; }
public string Titulo { get; }
public bool Feita { get; private set; }
public Tarefa(int id, string titulo)
{
if (string.IsNullOrWhiteSpace(titulo))
{
throw new ArgumentException("A tarefa precisa de um título.", nameof(titulo));
}
Id = id;
Titulo = titulo.Trim();
}
public void Concluir()
{
// FALTA AQUI: marcar a tarefa como feita
}
public override string ToString() => $"[{(Feita ? "x" : " ")}] {Id}. {Titulo}";
}
// Tarefa.cs — uma tarefa da lista
public class Tarefa
{
public int Id { get; }
public string Titulo { get; }
public bool Feita { get; private set; }
public Tarefa(int id, string titulo)
{
if (string.IsNullOrWhiteSpace(titulo))
{
throw new ArgumentException("A tarefa precisa de um título.", nameof(titulo));
}
Id = id;
Titulo = titulo.Trim();
}
public void Concluir()
{
Feita = true;
}
public override string ToString() => $"[{(Feita ? "x" : " ")}] {Id}. {Titulo}";
}
Versão 2 · A classe ListaDeTarefas e o menu
Na versão 1, o Program.cs mexia direto na List: escolhia o número de cada tarefa, contava as pendentes. Com o menu, essas regras iriam se espalhar pelo programa. A solução é uma segunda classe, que guarda a lista e é a única que mexe nela. Pense numa secretária que cuida do quadro de avisos: a Ana diz "anota isso" ou "risca a 2", e a secretária escolhe o número e cola o papel.
// ListaDeTarefas.cs — guarda as tarefas e dá os números
public class ListaDeTarefas
{
private readonly List<Tarefa> _tarefas = [];
private int _proximoId = 1;
public int Total => _tarefas.Count;
public int Pendentes => _tarefas.Count(t => !t.Feita);
public Tarefa Adicionar(string titulo)
{
Tarefa nova = new(_proximoId, titulo);
_tarefas.Add(nova);
_proximoId++;
return nova;
}
public Tarefa? Buscar(int id) => _tarefas.Find(t => t.Id == id);
public void Mostrar()
{
if (Total == 0)
{
Console.WriteLine("Nenhuma tarefa. Aproveite o dia!");
return;
}
foreach (Tarefa t in _tarefas)
{
Console.WriteLine(t);
}
Console.WriteLine($"{Pendentes} de {Total} por fazer.");
}
}
| Linha | O que faz |
|---|---|
private readonly List<Tarefa> _tarefas = []; | A lista de verdade, escondida (private). O readonly impede trocar a lista inteira por outra; mexer no conteúdo (Add, Remove) continua valendo. O _ no começo é a convenção do C# para campos privados (aula Campos). |
private int _proximoId = 1; | O número que a próxima tarefa vai receber. Só a lista sabe; a Ana não precisa escolher. |
public int Total => _tarefas.Count; | Uma propriedade calculada: a cada leitura, conta de novo. Não tem set. |
public int Pendentes => _tarefas.Count(t => !t.Feita); | Quantas não estão feitas. O Count(…) com uma condição é do LINQ; o ! é o "não". |
public Tarefa Adicionar(string titulo) | Cria a tarefa com o próximo número, guarda e devolve a tarefa nova, para quem chamou poder mostrá-la. |
Tarefa nova = new(_proximoId, titulo); | O new(…) sem o nome da classe funciona porque o tipo já está escrito à esquerda C# 9. |
public Tarefa? Buscar(int id) => _tarefas.Find(t => t.Id == id); | Find é da própria List: devolve a primeira tarefa com aquele número, ou null se não houver. O Tarefa? avisa quem chama que pode vir null (aula Referências Anuláveis). |
public void Mostrar() | Mostra a lista, ou um aviso quando está vazia. |
O Program.cs agora só conversa com a Ana. Toda regra fica na lista:
// Program.cs — versão 2: o menu
ListaDeTarefas lista = new();
Console.WriteLine("=== Lista de Tarefas ===");
while (true)
{
Console.WriteLine();
Console.WriteLine("1) Listar 2) Adicionar 3) Concluir 0) Sair");
Console.Write("Opção: ");
string? opcao = Console.ReadLine();
if (opcao is null || opcao == "0")
{
break;
}
switch (opcao)
{
case "1":
lista.Mostrar();
break;
case "2":
Console.Write("Título: ");
string titulo = Console.ReadLine() ?? "";
if (string.IsNullOrWhiteSpace(titulo))
{
Console.WriteLine("O título não pode ficar vazio.");
break;
}
Tarefa nova = lista.Adicionar(titulo);
Console.WriteLine($"Anotado: {nova}");
break;
case "3":
Console.Write("Número da tarefa: ");
if (!int.TryParse(Console.ReadLine(), out int id))
{
Console.WriteLine("Digite o número que aparece na lista.");
break;
}
Tarefa? t = lista.Buscar(id);
if (t is null)
{
Console.WriteLine($"Não há tarefa {id}.");
}
else if (t.Feita)
{
Console.WriteLine($"A tarefa {id} já estava feita.");
}
else
{
t.Concluir();
Console.WriteLine($"Feito: {t}");
}
break;
default:
Console.WriteLine("Opção inválida.");
break;
}
}
Console.WriteLine($"Até logo! Por fazer: {lista.Pendentes}.");
| Linha | O que faz |
|---|---|
ListaDeTarefas lista = new(); | Cria a lista vazia. |
string? opcao = Console.ReadLine(); | Lê a opção. O ReadLine devolve null quando a entrada acaba (Ctrl+D), por isso o string?; o if trata null como "sair" (aula Ler do Teclado). |
Console.ReadLine() ?? "" | Se vier null, usa o texto vazio, e o IsNullOrWhiteSpace logo abaixo recusa. |
if (string.IsNullOrWhiteSpace(titulo)) { … break; } | Confere antes de chamar o Adicionar, para a exceção do construtor nunca acontecer. Aqui o break sai do switch, não do while. |
int.TryParse(Console.ReadLine(), out int id) | Converte o número digitado. Com abc, devolve false e o programa explica, em vez de cair (aula Parse e TryParse). |
Tarefa? t = lista.Buscar(id); | Pede a tarefa à lista e trata os três casos: não existe, já estava feita, ou conclui. |
A Ana rodou e anotou as tarefas da semana. Ela também apertou Enter sem digitar o título (uma vez) e pediu a tarefa 9, que não existe:
$ dotnet run === Lista de Tarefas === 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 1 Nenhuma tarefa. Aproveite o dia! 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 2 Título: Comprar pão Anotado: [ ] 1. Comprar pão 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 2 Título: Estudar C# Anotado: [ ] 2. Estudar C# 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 2 Título: O título não pode ficar vazio. 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 2 Título: Pagar a conta de luz Anotado: [ ] 3. Pagar a conta de luz 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 3 Número da tarefa: 2 Feito: [x] 2. Estudar C# 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 3 Número da tarefa: 2 A tarefa 2 já estava feita. 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 3 Número da tarefa: 9 Não há tarefa 9. 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 1 [ ] 1. Comprar pão [x] 2. Estudar C# [ ] 3. Pagar a conta de luz 2 de 3 por fazer. 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 0 Até logo! Por fazer: 2.
Para apagar tudo de uma vez, a Ana tentou usar a List que está dentro da ListaDeTarefas:
ListaDeTarefas lista = new();
lista.Adicionar("Comprar pão");
lista._tarefas.Clear();
lista.Mostrar();
$ dotnet run /home/ana/projetos/Tarefas/Program.cs(3,7): error CS0122: "ListaDeTarefas._tarefas" é inacessível devido ao seu nível de proteção [/home/ana/projetos/Tarefas/Tarefas.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: o _tarefas é private: só o código dentro da classe ListaDeTarefas o enxerga. Em inglês: 'ListaDeTarefas._tarefas' is inaccessible due to its protection level. Aula: public, private e internal.
Como consertar: não troque para public. Se a Ana precisa de "apagar tudo", a lista ganha um método para isso (por exemplo, public void LimparTudo() => _tarefas.Clear();). É assim que a versão 3 vai ganhar o Remover e o LimparFeitas.
Missão (versão 2): a propriedade Pendentes, na aba ListaDeTarefas.cs, sempre devolve 0. Troque o 0 por uma conta das tarefas que não estão feitas. A entrada já vem pronta: anota "Comprar pão" e "Estudar C#", conclui a 1, lista e sai. O certo é 1 de 2 por fazer.
// Program.cs — versão 2: o menu
ListaDeTarefas lista = new();
Console.WriteLine("=== Lista de Tarefas ===");
while (true)
{
Console.WriteLine();
Console.WriteLine("1) Listar 2) Adicionar 3) Concluir 0) Sair");
Console.Write("Opção: ");
string? opcao = Console.ReadLine();
if (opcao is null || opcao == "0")
{
break;
}
switch (opcao)
{
case "1":
lista.Mostrar();
break;
case "2":
Console.Write("Título: ");
string titulo = Console.ReadLine() ?? "";
if (string.IsNullOrWhiteSpace(titulo))
{
Console.WriteLine("O título não pode ficar vazio.");
break;
}
Tarefa nova = lista.Adicionar(titulo);
Console.WriteLine($"Anotado: {nova}");
break;
case "3":
Console.Write("Número da tarefa: ");
if (!int.TryParse(Console.ReadLine(), out int id))
{
Console.WriteLine("Digite o número que aparece na lista.");
break;
}
Tarefa? t = lista.Buscar(id);
if (t is null)
{
Console.WriteLine($"Não há tarefa {id}.");
}
else if (t.Feita)
{
Console.WriteLine($"A tarefa {id} já estava feita.");
}
else
{
t.Concluir();
Console.WriteLine($"Feito: {t}");
}
break;
default:
Console.WriteLine("Opção inválida.");
break;
}
}
Console.WriteLine($"Até logo! Por fazer: {lista.Pendentes}.");
// ListaDeTarefas.cs — guarda as tarefas e dá os números
public class ListaDeTarefas
{
private readonly List<Tarefa> _tarefas = [];
private int _proximoId = 1;
public int Total => _tarefas.Count;
public int Pendentes => 0; // FALTA AQUI: contar as que não estão feitas
public Tarefa Adicionar(string titulo)
{
Tarefa nova = new(_proximoId, titulo);
_tarefas.Add(nova);
_proximoId++;
return nova;
}
public Tarefa? Buscar(int id) => _tarefas.Find(t => t.Id == id);
public void Mostrar()
{
if (Total == 0)
{
Console.WriteLine("Nenhuma tarefa. Aproveite o dia!");
return;
}
foreach (Tarefa t in _tarefas)
{
Console.WriteLine(t);
}
Console.WriteLine($"{Pendentes} de {Total} por fazer.");
}
}
// Tarefa.cs — uma tarefa da lista
public class Tarefa
{
public int Id { get; }
public string Titulo { get; }
public bool Feita { get; private set; }
public Tarefa(int id, string titulo)
{
if (string.IsNullOrWhiteSpace(titulo))
{
throw new ArgumentException("A tarefa precisa de um título.", nameof(titulo));
}
Id = id;
Titulo = titulo.Trim();
}
public void Concluir()
{
Feita = true;
}
public override string ToString() => $"[{(Feita ? "x" : " ")}] {Id}. {Titulo}";
}
// ListaDeTarefas.cs — guarda as tarefas e dá os números
public class ListaDeTarefas
{
private readonly List<Tarefa> _tarefas = [];
private int _proximoId = 1;
public int Total => _tarefas.Count;
public int Pendentes => _tarefas.Count(t => !t.Feita);
public Tarefa Adicionar(string titulo)
{
Tarefa nova = new(_proximoId, titulo);
_tarefas.Add(nova);
_proximoId++;
return nova;
}
public Tarefa? Buscar(int id) => _tarefas.Find(t => t.Id == id);
public void Mostrar()
{
if (Total == 0)
{
Console.WriteLine("Nenhuma tarefa. Aproveite o dia!");
return;
}
foreach (Tarefa t in _tarefas)
{
Console.WriteLine(t);
}
Console.WriteLine($"{Pendentes} de {Total} por fazer.");
}
}
Versão 3 · Remover e limpar as feitas
A lista cresce, e as tarefas feitas viram entulho. A versão 3 ganha duas opções: apagar uma tarefa pelo número e limpar todas as feitas de uma vez.
Primeiro, o jeito que parece certo
A Ana pensou assim: "a tarefa 1 está na posição 0, a 2 na posição 1… então a tarefa id está na posição id - 1". E escreveu:
public bool Remover(int id)
{
_tarefas.RemoveAt(id - 1); // a tarefa 1 está na posição 0... será?
return true;
}
Para testar, ela anotou quatro tarefas e apagou a 1, depois a 3, depois a 4:
ListaDeTarefas lista = new();
lista.Adicionar("Comprar pão");
lista.Adicionar("Estudar C#");
lista.Adicionar("Pagar a conta de luz");
lista.Adicionar("Ligar para a vó");
lista.Remover(1); // apaga "Comprar pão"
lista.Mostrar();
Console.WriteLine("--- apagando a tarefa 3");
lista.Remover(3);
lista.Mostrar();
Console.WriteLine("--- apagando a tarefa 4");
lista.Remover(4);
lista.Mostrar();
$ dotnet run [ ] 2. Estudar C# [ ] 3. Pagar a conta de luz [ ] 4. Ligar para a vó 3 de 3 por fazer. --- apagando a tarefa 3 [ ] 2. Estudar C# [ ] 3. Pagar a conta de luz « a 3 continua aí 2 de 2 por fazer. « e a 4 sumiu! --- apagando a tarefa 4 Unhandled exception. System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index') at System.Collections.Generic.List`1.RemoveAt(Int32 index) at ListaDeTarefas.Remover(Int32 id) in /home/ana/projetos/Tarefas/ListaDeTarefas.cs:line 23 at Program.<Main>$(String[] args) in /home/ana/projetos/Tarefas/Program.cs:line 13
O que aconteceu: a primeira remoção funcionou. Mas, ao apagar a tarefa 1, as outras andaram uma casa para trás: a tarefa 3 foi para a posição 1, e a 4 para a posição 2. O RemoveAt(3 - 1) apagou a posição 2, que agora era a tarefa 4: a errada, e sem erro nenhum. Depois, o RemoveAt(4 - 1) pediu a posição 3 de uma lista com 2 itens, e aí sim veio a exceção: "o índice estava fora do intervalo; tem de ser não negativo e menor que o tamanho da coleção".
Como consertar: o número é uma etiqueta da tarefa, não o lugar dela na fila. Procure pela etiqueta: _tarefas.RemoveAll(t => t.Id == id). É também por isso que os números nunca são reaproveitados: depois de apagar a 4, a próxima tarefa é a 5, e "a tarefa 4" nunca vai querer dizer outra coisa.
O ListaDeTarefas.cs da versão 3 ganha dois métodos de uma linha, logo depois do Buscar:
// Devolve true se achou e apagou a tarefa com esse número.
public bool Remover(int id) => _tarefas.RemoveAll(t => t.Id == id) > 0;
// Apaga todas as feitas e devolve quantas apagou.
public int LimparFeitas() => _tarefas.RemoveAll(t => t.Feita);
| Linha | O que faz |
|---|---|
_tarefas.RemoveAll(t => t.Id == id) | Apaga todas as tarefas em que a condição é verdadeira e devolve quantas apagou. Como os números não se repetem, aqui é 0 ou 1. É o jeito seguro de apagar com condição (veja o cenário "Collection was modified" no projeto Estoque). |
… > 0 | Vira bool: true se apagou alguma coisa. |
_tarefas.RemoveAll(t => t.Feita) | Apaga todas as feitas, de uma vez. |
No Program.cs, o menu ganha as opções 4 e 5 (o resto é igual à versão 2):
case "4":
Console.Write("Número da tarefa: ");
if (int.TryParse(Console.ReadLine(), out int apagar) && lista.Remover(apagar))
{
Console.WriteLine($"Tarefa {apagar} apagada.");
}
else
{
Console.WriteLine("Não achei essa tarefa.");
}
break;
case "5":
int quantas = lista.LimparFeitas();
Console.WriteLine($"Apaguei {quantas} tarefas feitas.");
break;
O && só chama o Remover se o TryParse deu certo (aula Operadores Lógicos). Com o mesmo teste de antes, agora pelo número:
$ dotnet run [ ] 2. Estudar C# [ ] 3. Pagar a conta de luz [ ] 4. Ligar para a vó 3 de 3 por fazer. --- apagando a tarefa 3 [ ] 2. Estudar C# [ ] 4. Ligar para a vó 2 de 2 por fazer. --- apagando a tarefa 4 [ ] 2. Estudar C# 1 de 1 por fazer.
E pelo menu, numa tarde inteira (a Ana conclui a 2 e a 4, apaga a 1, limpa as feitas e anota uma nova, que recebe o número 5):
$ dotnet run === Lista de Tarefas === 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Título: Comprar pão Anotado: [ ] 1. Comprar pão 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Título: Estudar C# Anotado: [ ] 2. Estudar C# 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Título: Pagar a conta de luz Anotado: [ ] 3. Pagar a conta de luz 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Título: Ligar para a vó Anotado: [ ] 4. Ligar para a vó 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 3 Número da tarefa: 2 Feito: [x] 2. Estudar C# 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 3 Número da tarefa: 4 Feito: [x] 4. Ligar para a vó 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 4 Número da tarefa: 1 Tarefa 1 apagada. 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 1 [x] 2. Estudar C# [ ] 3. Pagar a conta de luz [x] 4. Ligar para a vó 1 de 3 por fazer. 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 5 Apaguei 2 tarefas feitas. 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Título: Regar as plantas Anotado: [ ] 5. Regar as plantas 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 1 [ ] 3. Pagar a conta de luz [ ] 5. Regar as plantas 2 de 2 por fazer. 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 0 Até logo! Por fazer: 2.
Missão (versão 3): o LimparFeitas da aba ListaDeTarefas.cs não apaga nada. Escreva-o com RemoveAll. A entrada anota três tarefas, conclui a 1 e a 3, limpa (opção 5), lista e sai. O certo é Apaguei 2 tarefas feitas. e sobrar só a tarefa 2.
// Program.cs — versão 3: remover e limpar as feitas
ListaDeTarefas lista = new();
Console.WriteLine("=== Lista de Tarefas ===");
while (true)
{
Console.WriteLine();
Console.WriteLine("1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair");
Console.Write("Opção: ");
string? opcao = Console.ReadLine();
if (opcao is null || opcao == "0")
{
break;
}
switch (opcao)
{
case "1":
lista.Mostrar();
break;
case "2":
Console.Write("Título: ");
string titulo = Console.ReadLine() ?? "";
if (string.IsNullOrWhiteSpace(titulo))
{
Console.WriteLine("O título não pode ficar vazio.");
break;
}
Tarefa nova = lista.Adicionar(titulo);
Console.WriteLine($"Anotado: {nova}");
break;
case "3":
Console.Write("Número da tarefa: ");
if (!int.TryParse(Console.ReadLine(), out int id))
{
Console.WriteLine("Digite o número que aparece na lista.");
break;
}
Tarefa? t = lista.Buscar(id);
if (t is null)
{
Console.WriteLine($"Não há tarefa {id}.");
}
else if (t.Feita)
{
Console.WriteLine($"A tarefa {id} já estava feita.");
}
else
{
t.Concluir();
Console.WriteLine($"Feito: {t}");
}
break;
case "4":
Console.Write("Número da tarefa: ");
if (int.TryParse(Console.ReadLine(), out int apagar) && lista.Remover(apagar))
{
Console.WriteLine($"Tarefa {apagar} apagada.");
}
else
{
Console.WriteLine("Não achei essa tarefa.");
}
break;
case "5":
int quantas = lista.LimparFeitas();
Console.WriteLine($"Apaguei {quantas} tarefas feitas.");
break;
default:
Console.WriteLine("Opção inválida.");
break;
}
}
Console.WriteLine($"Até logo! Por fazer: {lista.Pendentes}.");
// ListaDeTarefas.cs — guarda as tarefas e dá os números
public class ListaDeTarefas
{
private readonly List<Tarefa> _tarefas = [];
private int _proximoId = 1;
public int Total => _tarefas.Count;
public int Pendentes => _tarefas.Count(t => !t.Feita);
public Tarefa Adicionar(string titulo)
{
Tarefa nova = new(_proximoId, titulo);
_tarefas.Add(nova);
_proximoId++;
return nova;
}
public Tarefa? Buscar(int id) => _tarefas.Find(t => t.Id == id);
// Devolve true se achou e apagou a tarefa com esse número.
public bool Remover(int id) => _tarefas.RemoveAll(t => t.Id == id) > 0;
// Apaga todas as feitas e devolve quantas apagou.
public int LimparFeitas() => 0; // FALTA AQUI: RemoveAll das feitas
public void Mostrar()
{
if (Total == 0)
{
Console.WriteLine("Nenhuma tarefa. Aproveite o dia!");
return;
}
foreach (Tarefa t in _tarefas)
{
Console.WriteLine(t);
}
Console.WriteLine($"{Pendentes} de {Total} por fazer.");
}
}
// Tarefa.cs — uma tarefa da lista
public class Tarefa
{
public int Id { get; }
public string Titulo { get; }
public bool Feita { get; private set; }
public Tarefa(int id, string titulo)
{
if (string.IsNullOrWhiteSpace(titulo))
{
throw new ArgumentException("A tarefa precisa de um título.", nameof(titulo));
}
Id = id;
Titulo = titulo.Trim();
}
public void Concluir()
{
Feita = true;
}
public override string ToString() => $"[{(Feita ? "x" : " ")}] {Id}. {Titulo}";
}
// ListaDeTarefas.cs — guarda as tarefas e dá os números
public class ListaDeTarefas
{
private readonly List<Tarefa> _tarefas = [];
private int _proximoId = 1;
public int Total => _tarefas.Count;
public int Pendentes => _tarefas.Count(t => !t.Feita);
public Tarefa Adicionar(string titulo)
{
Tarefa nova = new(_proximoId, titulo);
_tarefas.Add(nova);
_proximoId++;
return nova;
}
public Tarefa? Buscar(int id) => _tarefas.Find(t => t.Id == id);
// Devolve true se achou e apagou a tarefa com esse número.
public bool Remover(int id) => _tarefas.RemoveAll(t => t.Id == id) > 0;
// Apaga todas as feitas e devolve quantas apagou.
public int LimparFeitas() => _tarefas.RemoveAll(t => t.Feita);
public void Mostrar()
{
if (Total == 0)
{
Console.WriteLine("Nenhuma tarefa. Aproveite o dia!");
return;
}
foreach (Tarefa t in _tarefas)
{
Console.WriteLine(t);
}
Console.WriteLine($"{Pendentes} de {Total} por fazer.");
}
}
Versão 4 · A lista guardada em tarefas.txt
Falta o principal: ao sair, a lista some. A versão 4 grava as tarefas num arquivo de texto, uma por linha, e lê de volta quando o programa abre. Cada linha tem três campos separados por |: o número, 1 ou 0 (feita ou não) e o título.
1|0|Comprar pão 2|1|Estudar C# 3|0|Pagar a conta de luz
O título fica por último de propósito: se a Ana escrever uma tarefa com | no texto ("Feira: pão|leite"), o programa corta só nos dois primeiros | e o resto inteiro é o título.
A Tarefa ganha um parâmetro opcional no construtor, para criar uma tarefa que já vem feita do arquivo (aula Parâmetros Opcionais):
// Tarefa.cs — uma tarefa da lista
public class Tarefa
{
public int Id { get; }
public string Titulo { get; }
public bool Feita { get; private set; }
public Tarefa(int id, string titulo, bool feita = false)
{
if (string.IsNullOrWhiteSpace(titulo))
{
throw new ArgumentException("A tarefa precisa de um título.", nameof(titulo));
}
Id = id;
Titulo = titulo.Trim();
Feita = feita;
}
public void Concluir()
{
Feita = true;
}
public override string ToString() => $"[{(Feita ? "x" : " ")}] {Id}. {Titulo}";
}
A ListaDeTarefas ganha um construtor que recebe o nome do arquivo e carrega, e os métodos Salvar e Carregar:
// ListaDeTarefas.cs — guarda as tarefas, num arquivo também
public class ListaDeTarefas
{
private readonly List<Tarefa> _tarefas = [];
private int _proximoId = 1;
private readonly string _arquivo;
public ListaDeTarefas(string arquivo)
{
_arquivo = arquivo;
Carregar();
}
public int Total => _tarefas.Count;
public int Pendentes => _tarefas.Count(t => !t.Feita);
public Tarefa Adicionar(string titulo)
{
Tarefa nova = new(_proximoId, titulo);
_tarefas.Add(nova);
_proximoId++;
return nova;
}
public Tarefa? Buscar(int id) => _tarefas.Find(t => t.Id == id);
// Devolve true se achou e apagou a tarefa com esse número.
public bool Remover(int id) => _tarefas.RemoveAll(t => t.Id == id) > 0;
// Apaga todas as feitas e devolve quantas apagou.
public int LimparFeitas() => _tarefas.RemoveAll(t => t.Feita);
public void Mostrar()
{
if (Total == 0)
{
Console.WriteLine("Nenhuma tarefa. Aproveite o dia!");
return;
}
foreach (Tarefa t in _tarefas)
{
Console.WriteLine(t);
}
Console.WriteLine($"{Pendentes} de {Total} por fazer.");
}
// Uma linha por tarefa: número|feita (1 ou 0)|título
public void Salvar()
{
using StreamWriter escritor = new(_arquivo);
foreach (Tarefa t in _tarefas)
{
escritor.WriteLine($"{t.Id}|{(t.Feita ? 1 : 0)}|{t.Titulo}");
}
}
private void Carregar()
{
if (!File.Exists(_arquivo))
{
return; // primeira vez: lista vazia
}
foreach (string linha in File.ReadAllLines(_arquivo))
{
string[] partes = linha.Split('|', 3);
if (partes.Length != 3 || !int.TryParse(partes[0], out int id))
{
continue; // linha estragada: pula
}
_tarefas.Add(new Tarefa(id, partes[2], partes[1] == "1"));
}
if (_tarefas.Count > 0)
{
_proximoId = _tarefas.Max(t => t.Id) + 1;
}
}
}
| Linha | O que faz |
|---|---|
bool feita = false | Parâmetro opcional: new Tarefa(1, "Comprar pão") continua valendo (feita = false), e o Carregar passa o terceiro valor. |
public ListaDeTarefas(string arquivo) | O construtor guarda o nome do arquivo e já carrega o que houver nele. |
using StreamWriter escritor = new(_arquivo); | Abre o arquivo para escrever (cria, ou apaga o que havia). O using garante que ele será fechado no fim do método, mesmo se der erro no meio, e fechar é o que grava de verdade (veja o cenário abaixo; aula using e IDisposable). C# 8 |
escritor.WriteLine($"{t.Id}|{(t.Feita ? 1 : 0)}|{t.Titulo}"); | Uma linha por tarefa: 2|1|Estudar C#. |
if (!File.Exists(_arquivo)) { return; } | Na primeira vez, o arquivo não existe: a lista começa vazia, sem erro. |
File.ReadAllLines(_arquivo) | Lê o arquivo inteiro e devolve um array com uma string por linha (aula Arquivos). |
linha.Split('|', 3) | Corta no |, em no máximo 3 pedaços: o terceiro fica com todo o resto da linha, mesmo que tenha |. |
if (partes.Length != 3 || !int.TryParse(…)) { continue; } | Uma linha estragada (vazia, ou com o número errado) é pulada em vez de derrubar o programa. |
partes[1] == "1" | Vira true se o campo do meio é 1. |
_proximoId = _tarefas.Max(t => t.Id) + 1; | Continua a numeração de onde parou: se a maior tarefa do arquivo é a 3, a próxima é a 4. |
No Program.cs, mudam três coisas: a lista é criada com o nome do arquivo, a primeira linha mostra o que foi carregado, e o programa salva logo depois de cada mudança (opções 2 a 5). Assim, mesmo se a Ana fechar o terminal sem digitar 0, nada se perde:
// Program.cs — versão 4: a lista guardada em tarefas.txt
ListaDeTarefas lista = new("tarefas.txt");
Console.WriteLine("=== Lista de Tarefas ===");
Console.WriteLine($"{lista.Total} tarefas no caderno, {lista.Pendentes} por fazer.");
while (true)
{
Console.WriteLine();
Console.WriteLine("1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair");
Console.Write("Opção: ");
string? opcao = Console.ReadLine();
if (opcao is null || opcao == "0")
{
break;
}
switch (opcao)
{
case "1":
lista.Mostrar();
break;
case "2":
Console.Write("Título: ");
string titulo = Console.ReadLine() ?? "";
if (string.IsNullOrWhiteSpace(titulo))
{
Console.WriteLine("O título não pode ficar vazio.");
break;
}
Tarefa nova = lista.Adicionar(titulo);
Console.WriteLine($"Anotado: {nova}");
break;
case "3":
Console.Write("Número da tarefa: ");
if (!int.TryParse(Console.ReadLine(), out int id))
{
Console.WriteLine("Digite o número que aparece na lista.");
break;
}
Tarefa? t = lista.Buscar(id);
if (t is null)
{
Console.WriteLine($"Não há tarefa {id}.");
}
else if (t.Feita)
{
Console.WriteLine($"A tarefa {id} já estava feita.");
}
else
{
t.Concluir();
Console.WriteLine($"Feito: {t}");
}
break;
case "4":
Console.Write("Número da tarefa: ");
if (int.TryParse(Console.ReadLine(), out int apagar) && lista.Remover(apagar))
{
Console.WriteLine($"Tarefa {apagar} apagada.");
}
else
{
Console.WriteLine("Não achei essa tarefa.");
}
break;
case "5":
int quantas = lista.LimparFeitas();
Console.WriteLine($"Apaguei {quantas} tarefas feitas.");
break;
default:
Console.WriteLine("Opção inválida.");
break;
}
if (opcao is "2" or "3" or "4" or "5")
{
lista.Salvar(); // guarda a cada mudança
}
}
Console.WriteLine($"Até logo! Por fazer: {lista.Pendentes}.");
O opcao is "2" or "3" or "4" or "5" é um padrão: "a opção é 2, ou 3, ou 4, ou 5" C# 9. No primeiro dia, ainda não há arquivo:
$ dotnet run === Lista de Tarefas === 0 tarefas no caderno, 0 por fazer. 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Título: Comprar pão Anotado: [ ] 1. Comprar pão 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Título: Estudar C# Anotado: [ ] 2. Estudar C# 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Título: Pagar a conta de luz Anotado: [ ] 3. Pagar a conta de luz 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 3 Número da tarefa: 2 Feito: [x] 2. Estudar C# 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 0 Até logo! Por fazer: 2. $ cat tarefas.txt 1|0|Comprar pão 2|1|Estudar C# 3|0|Pagar a conta de luz
No dia seguinte, o programa começa com 3 tarefas no caderno, 2 por fazer.: é a sessão do começo da página (O que vamos construir). Depois dela, o arquivo ficou assim, com a tarefa 4 nova e sem a 1 e a 2, que foram limpas:
$ cat tarefas.txt 3|0|Pagar a conta de luz 4|0|Ligar para a vó
Na primeira versão do Carregar, a Ana não tinha o File.Exists. No primeiro dia, ainda sem arquivo:
$ dotnet run Unhandled exception. System.IO.FileNotFoundException: Could not find file '/home/ana/projetos/Tarefas/tarefas.txt'. File name: '/home/ana/projetos/Tarefas/tarefas.txt' at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirError) … « 5 linhas de dentro do .NET at System.IO.File.ReadAllLines(String path, Encoding encoding) at ListaDeTarefas.Carregar() in /home/ana/projetos/Tarefas/ListaDeTarefas.cs:line 59 at ListaDeTarefas..ctor(String arquivo) in /home/ana/projetos/Tarefas/ListaDeTarefas.cs:line 11 at Program.<Main>$(String[] args) in /home/ana/projetos/Tarefas/Program.cs:line 2
O que significa: "não achei o arquivo". O rastro é comprido, mas leia de baixo para cima até achar um arquivo seu: Program.cs linha 2 criou a lista, o construtor chamou o Carregar, e a linha 59 chamou o File.ReadAllLines. O caminho completo mostra onde o programa procurou: na pasta em que a Ana rodou o dotnet run.
Como consertar: não existir é normal na primeira vez. Confira com File.Exists e comece vazio. Para outros problemas de arquivo (sem permissão, disco cheio), o caminho é o try e catch.
using, o arquivo fica vazio
Numa das tentativas, a Ana escreveu o Salvar sem o using (só StreamWriter escritor = new(_arquivo);). O programa compilou, rodou e disse que salvou. Este Program.cs de teste anota duas tarefas e salva:
ListaDeTarefas lista = new("tarefas.txt");
lista.Adicionar("Comprar pão");
lista.Adicionar("Estudar C#");
lista.Salvar();
Console.WriteLine($"Salvei {lista.Total} tarefas.");
$ dotnet run Salvei 2 tarefas. $ ls -l tarefas.txt -rw-rw-r-- 1 ana ana 0 out 17 12:59 tarefas.txt $ cat tarefas.txt
O que aconteceu: o arquivo tem 0 bytes. O StreamWriter não escreve no disco a cada WriteLine: ele junta o texto num "balde" na memória e esvazia o balde de uma vez, quando enche ou quando é fechado. Sem o using, ninguém fechou o arquivo; o programa terminou com o balde cheio, e o texto foi embora com ele. Não houve erro nem aviso.
Como consertar: todo StreamWriter (e todo objeto que abre um arquivo) vai com using. Com o using de volta, o mesmo teste grava as duas linhas (32 bytes):
$ dotnet run Salvei 2 tarefas. $ cat tarefas.txt 1|0|Comprar pão 2|0|Estudar C#
Missão (versão 4): a aba tarefas.txt já tem duas tarefas. O programa carrega, anota "Regar as plantas", salva e mostra o arquivo. Mas o Salvar da aba ListaDeTarefas.cs está sem o using, e o arquivo sai vazio. Conserte para o arquivo mostrar as três tarefas, com 3|0|Regar as plantas no fim.
ListaDeTarefas lista = new("tarefas.txt");
Console.WriteLine($"Carreguei {lista.Total} tarefas.");
lista.Adicionar("Regar as plantas");
lista.Salvar();
Console.WriteLine("--- tarefas.txt agora:");
Console.Write(File.ReadAllText("tarefas.txt"));
Console.WriteLine("--- fim do arquivo");
// ListaDeTarefas.cs — guarda as tarefas, num arquivo também
public class ListaDeTarefas
{
private readonly List<Tarefa> _tarefas = [];
private int _proximoId = 1;
private readonly string _arquivo;
public ListaDeTarefas(string arquivo)
{
_arquivo = arquivo;
Carregar();
}
public int Total => _tarefas.Count;
public int Pendentes => _tarefas.Count(t => !t.Feita);
public Tarefa Adicionar(string titulo)
{
Tarefa nova = new(_proximoId, titulo);
_tarefas.Add(nova);
_proximoId++;
return nova;
}
public Tarefa? Buscar(int id) => _tarefas.Find(t => t.Id == id);
// Devolve true se achou e apagou a tarefa com esse número.
public bool Remover(int id) => _tarefas.RemoveAll(t => t.Id == id) > 0;
// Apaga todas as feitas e devolve quantas apagou.
public int LimparFeitas() => _tarefas.RemoveAll(t => t.Feita);
public void Mostrar()
{
if (Total == 0)
{
Console.WriteLine("Nenhuma tarefa. Aproveite o dia!");
return;
}
foreach (Tarefa t in _tarefas)
{
Console.WriteLine(t);
}
Console.WriteLine($"{Pendentes} de {Total} por fazer.");
}
// Uma linha por tarefa: número|feita (1 ou 0)|título
public void Salvar()
{
StreamWriter escritor = new(_arquivo);
foreach (Tarefa t in _tarefas)
{
escritor.WriteLine($"{t.Id}|{(t.Feita ? 1 : 0)}|{t.Titulo}");
}
}
private void Carregar()
{
if (!File.Exists(_arquivo))
{
return; // primeira vez: lista vazia
}
foreach (string linha in File.ReadAllLines(_arquivo))
{
string[] partes = linha.Split('|', 3);
if (partes.Length != 3 || !int.TryParse(partes[0], out int id))
{
continue; // linha estragada: pula
}
_tarefas.Add(new Tarefa(id, partes[2], partes[1] == "1"));
}
if (_tarefas.Count > 0)
{
_proximoId = _tarefas.Max(t => t.Id) + 1;
}
}
}
// Tarefa.cs — uma tarefa da lista
public class Tarefa
{
public int Id { get; }
public string Titulo { get; }
public bool Feita { get; private set; }
public Tarefa(int id, string titulo, bool feita = false)
{
if (string.IsNullOrWhiteSpace(titulo))
{
throw new ArgumentException("A tarefa precisa de um título.", nameof(titulo));
}
Id = id;
Titulo = titulo.Trim();
Feita = feita;
}
public void Concluir()
{
Feita = true;
}
public override string ToString() => $"[{(Feita ? "x" : " ")}] {Id}. {Titulo}";
}
1|0|Comprar pão 2|1|Estudar C#
// ListaDeTarefas.cs — guarda as tarefas, num arquivo também
public class ListaDeTarefas
{
private readonly List<Tarefa> _tarefas = [];
private int _proximoId = 1;
private readonly string _arquivo;
public ListaDeTarefas(string arquivo)
{
_arquivo = arquivo;
Carregar();
}
public int Total => _tarefas.Count;
public int Pendentes => _tarefas.Count(t => !t.Feita);
public Tarefa Adicionar(string titulo)
{
Tarefa nova = new(_proximoId, titulo);
_tarefas.Add(nova);
_proximoId++;
return nova;
}
public Tarefa? Buscar(int id) => _tarefas.Find(t => t.Id == id);
// Devolve true se achou e apagou a tarefa com esse número.
public bool Remover(int id) => _tarefas.RemoveAll(t => t.Id == id) > 0;
// Apaga todas as feitas e devolve quantas apagou.
public int LimparFeitas() => _tarefas.RemoveAll(t => t.Feita);
public void Mostrar()
{
if (Total == 0)
{
Console.WriteLine("Nenhuma tarefa. Aproveite o dia!");
return;
}
foreach (Tarefa t in _tarefas)
{
Console.WriteLine(t);
}
Console.WriteLine($"{Pendentes} de {Total} por fazer.");
}
// Uma linha por tarefa: número|feita (1 ou 0)|título
public void Salvar()
{
using StreamWriter escritor = new(_arquivo);
foreach (Tarefa t in _tarefas)
{
escritor.WriteLine($"{t.Id}|{(t.Feita ? 1 : 0)}|{t.Titulo}");
}
}
private void Carregar()
{
if (!File.Exists(_arquivo))
{
return; // primeira vez: lista vazia
}
foreach (string linha in File.ReadAllLines(_arquivo))
{
string[] partes = linha.Split('|', 3);
if (partes.Length != 3 || !int.TryParse(partes[0], out int id))
{
continue; // linha estragada: pula
}
_tarefas.Add(new Tarefa(id, partes[2], partes[1] == "1"));
}
if (_tarefas.Count > 0)
{
_proximoId = _tarefas.Max(t => t.Id) + 1;
}
}
}
Formas diferentes
| Em vez de… | Daria para… | Qual usar |
|---|---|---|
class Tarefa com private set | record Tarefa(int Id, string Titulo, bool Feita) e with { Feita = true } | O record é ótimo para dados que só se leem (o projeto Estoque usa). Aqui a tarefa tem regra (o título não pode ser vazio) e uma ação (Concluir): a classe deixa isso mais claro. |
Uma ListaDeTarefas que esconde a List | Uma List<Tarefa> solta no Program.cs | Funciona para programas pequenos (a versão 1 fez assim). Quando as regras crescem (números, salvar a cada mudança), uma classe que guarda a lista evita repetir código e erros como o do RemoveAt. |
tarefas.txt com | | tarefas.json com JsonSerializer | O JSON lida sozinho com textos que têm |, quebras de linha e aspas (aula JSON; o projeto Agenda usa). O texto com | se lê e se conserta à mão, num editor. |
using StreamWriter escritor = new(…); | File.WriteAllLines(_arquivo, linhas) | Também fecha o arquivo sozinho e é mais curto. O StreamWriter foi usado para mostrar o using, que você vai precisar com qualquer arquivo aberto aos poucos. |
As classes sem namespace | namespace Tarefas; no topo de cada arquivo | Num projeto maior, use o namespace (aula Namespaces e using). Num projeto de três arquivos, ele não muda nada. |
Desafios extras
- Editar: uma opção que troca o título de uma tarefa. Como o
Titulosó temget, crie naTarefaum métodoRenomear(string novo)que valida como o construtor (troque o{ get; }por{ get; private set; }). - Data de conclusão: uma propriedade
DateTime? FeitaEm, preenchida noConcluir(aula Datas e Horas). Grave no arquivo como um quarto campo… e pense em onde ele deve ficar, por causa doSplit('|', 3). - Desfazer: uma opção que volta uma tarefa feita para pendente.
- Ordem: listar primeiro as pendentes, depois as feitas, com
OrderBy(t => t.Feita)(ofalsevem antes dotrue).
Exercícios
1. Para que serve o private set em public bool Feita { get; private set; }?
2. Depois de apagar a tarefa 1 de uma lista com as tarefas 1, 2, 3 e 4, em que posição da List está a tarefa 3?
3. O Salvar sem using deixou o tarefas.txt com 0 bytes. Por quê?
4. O que "5|0|Feira: pão|leite".Split('|', 3) devolve?
5. Complete a propriedade que todos podem ler e só a classe pode mudar:
6. Complete para apagar todas as tarefas feitas e saber quantas foram:
7. Complete para o arquivo ser fechado (e gravado) no fim do método:
Resumo
- Cada classe no seu arquivo; o projeto compila todos os
.csda pasta juntos. { get; }só de leitura,{ get; private set; }lida por todos e mudada só pela classe. Ações viram métodos (Concluir).- O construtor valida e recusa dados ruins com
throw new ArgumentException(…); quem chama confere antes. override ToString()decide como o objeto aparece noConsole.WriteLine.- Uma classe que guarda a
Listcomoprivateconcentra as regras; quem está fora pede por métodos. - O número de uma tarefa é uma etiqueta, não a posição: procure com
Finde apague comRemoveAll. - Arquivo: confira
File.Exists, leia comFile.ReadAllLines, grave comStreamWritersempre comusing.