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.

Você vai praticar

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.
  1. A classe Tarefa (propriedades, construtor, ToString).
  2. A classe ListaDeTarefas e o menu (adicionar e concluir).
  3. Remover e limpar as feitas (RemoveAll, e o número que não é a posição).
  4. A lista guardada em tarefas.txt (StreamWriter e using).

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:

ArquivoO que temDesde
Tarefa.csA classe Tarefa: uma tarefa só.versão 1
ListaDeTarefas.csA classe ListaDeTarefas: todas as tarefas, e as regras de adicionar, concluir, apagar e salvar.versão 2
Program.csO menu: conversa com a Ana e chama a lista.versão 1
tarefas.txtA 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}";
}
LinhaO que faz
public class TarefaDeclara 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.

CS0272: o 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.

ArgumentException: a tarefa sem título
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.");
    }
}
LinhaO 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}.");
LinhaO 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.
CS0122: o campo é privado

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:

O número da tarefa não é a posição na lista
    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);
LinhaO 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).
… > 0Vira 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;
        }
    }
}
LinhaO que faz
bool feita = falseParâ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ó
Primeira vez: FileNotFoundException

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.

Sem o 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 setrecord 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 ListUma List<Tarefa> solta no Program.csFunciona 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 JsonSerializerO 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 namespacenamespace Tarefas; no topo de cada arquivoNum 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 Titulo só tem get, crie na Tarefa um método Renomear(string novo) que valida como o construtor (troque o { get; } por { get; private set; }).
  • Data de conclusão: uma propriedade DateTime? FeitaEm, preenchida no Concluir (aula Datas e Horas). Grave no arquivo como um quarto campo… e pense em onde ele deve ficar, por causa do Split('|', 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) (o false vem antes do true).

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:

public bool Feita { get; }

6. Complete para apagar todas as tarefas feitas e saber quantas foram:

int quantas = _tarefas.(t => t.Feita);

7. Complete para o arquivo ser fechado (e gravado) no fim do método:

StreamWriter escritor = new("tarefas.txt");

Resumo

  • Cada classe no seu arquivo; o projeto compila todos os .cs da 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 no Console.WriteLine.
  • Uma classe que guarda a List como private concentra as regras; quem está fora pede por métodos.
  • O número de uma tarefa é uma etiqueta, não a posição: procure com Find e apague com RemoveAll.
  • Arquivo: confira File.Exists, leia com File.ReadAllLines, grave com StreamWriter sempre com using.