Projeto Estoque da Lojinha
A Ana tem uma lojinha de papelaria e controla o estoque num caderno: quantos cadernos, canetas e mochilas ainda tem, e quanto custa cada um. Toda semana ela faz as mesmas perguntas: "o que está acabando?", "quanto vale tudo o que está na prateleira?", "qual categoria dá mais dinheiro?". Neste projeto, ela troca o caderno por um programa em C# que responde a essas perguntas com LINQ, em uma linha cada, e que guarda o estoque num arquivo para não esquecer nada de um dia para o outro.
- Descrever um produto com um record e guardar todos numa
List<T>. - Mostrar uma tabela alinhada com interpolação e o formato de dinheiro
:C(Formatar Números). - Fazer perguntas à lista com LINQ:
Where,OrderBy,Select,Count,SumeMaxBy. - Agrupar por categoria com
GroupBye procurar comFirstOrDefault. - Fazer um menu com
Console.ReadLineeint.TryParse, e "mudar" um record comwith. - Guardar o estoque em
estoque.csvsem perder os centavos por causa da cultura pt-BR.
O que vamos construir
O programa pronto, na máquina da Ana. Na primeira vez, ainda não existe o arquivo, e ele começa com os produtos do caderno de papel. Ela vê o que está acabando, recebe 20 borrachas, vende 5 canetas, tenta vender 3 mochilas (só há 2) e sai:
$ dotnet run estoque.csv não existe: começando do caderno de papel. === Estoque da Lojinha === 7 produtos carregados. 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 2 Cód Produto Qtd Preço 3 Borracha 3 R$ 1,75 4 Mochila 2 R$ 89,90 6 Papel A4 4 R$ 27,50 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 3 Código: 3 Quantas unidades? 20 Borracha: agora são 23. 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 4 Código: 2 Quantas unidades? 5 Caneta: agora são 35. 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 4 Código: 4 Quantas unidades? 3 Só há 2 de Mochila. 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 0 Salvei 7 produtos em estoque.csv. Até amanhã!
No dia seguinte, o programa lê o arquivo e continua de onde parou: a caneta tem 35, e a borracha, 23.
- A ficha do produto e a lista (
record,Liste a tabela). - Perguntas ao estoque com LINQ (
Where,OrderBy,Sum…). - O relatório por categoria e a busca (
GroupByeFirstOrDefault). - O menu: entrada e saída (
TryParse,FindIndexewith). - O estoque guardado num arquivo (
estoque.csv).
Antes de começar
Você precisa ter visto List<T>, Records, Lambdas e as aulas de LINQ, a partir de O que é LINQ. Como o programa vai crescer, a Ana cria um projeto (aula Projetos):
$ mkdir -p projetos && cd projetos $ dotnet new console -o Estoque O modelo "Aplicativo do Console" foi criado com êxito. Processando ações pós-criação... Restaurando /home/ana/projetos/Estoque/Estoque.csproj: Determinando os projetos a serem restaurados... /home/ana/projetos/Estoque/Estoque.csproj restaurado (em 107 ms). A restauração foi bem-sucedida. $ cd Estoque
Em cada versão, ela troca todo o conteúdo do Program.cs pelo código novo e roda com dotnet run, dentro da pasta ~/projetos/Estoque.
Versão 1 · A ficha do produto e a lista
Pense nas fichas de papel da Ana: cada produto tem uma, com os mesmos campos na mesma ordem (código, nome, categoria, quantidade e preço). Em C#, o desenho da ficha é um record, e a caixa com todas as fichas é uma List<Produto> ("lista de produtos").
A primeira versão não pergunta nada ao usuário. Ela só monta a lista e mostra a tabela do estoque:
// Estoque da Lojinha — versão 1: a ficha e a lista
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
Console.WriteLine("=== Estoque da Lojinha ===");
Console.WriteLine("Cód Produto Categoria Qtd Preço");
foreach (Produto p in estoque)
{
Console.WriteLine($"{p.Codigo,3} {p.Nome,-10} {p.Categoria,-10} {p.Quantidade,3} {p.Preco,10:C}");
}
Console.WriteLine($"{estoque.Count} produtos cadastrados.");
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
| Linha | O que faz |
|---|---|
record Produto(int Codigo, string Nome, …, decimal Preco); | O desenho da ficha, na última linha do arquivo. Esta linha cria o tipo Produto, com cinco propriedades e um construtor que recebe as cinco, na ordem. Em Program.cs, os tipos ficam depois das instruções (aula Program.cs e o Main). C# 9 |
decimal Preco | Dinheiro é sempre decimal: ele guarda 15,90 exato, e o double não (aula float, double e decimal). |
List<Produto> estoque = [ … ]; | Cria a lista já com sete produtos. Os colchetes são uma expressão de coleção C# 12; antes dela, escrevia-se new List<Produto> { … }. |
new(1, "Caderno", "Papelaria", 12, 15.90m) | Cria um Produto. O C# já sabe o tipo (é uma lista de Produto), por isso basta new(…) C# 9. O m no fim de 15.90m diz "isto é um decimal". No código, o separador decimal é sempre o ponto. |
foreach (Produto p in estoque) | Passa por cada ficha, da primeira à última. Dentro do laço, p é o produto da vez (aula foreach). |
{p.Codigo,3} | O número depois da vírgula é a largura: o código ocupa 3 posições, alinhado à direita. |
{p.Nome,-10} | Largura negativa alinha à esquerda: o nome ocupa 10 posições, e o que sobra vira espaço. |
{p.Preco,10:C} | Largura 10 e o formato C ("currency", moeda). Na máquina da Ana, em pt-BR, 15.90m sai R$ 15,90. |
estoque.Count | Quantos itens a lista tem agora. É uma propriedade (sem parênteses). |
$ dotnet run === Estoque da Lojinha === Cód Produto Categoria Qtd Preço 1 Caderno Papelaria 12 R$ 15,90 2 Caneta Escrita 40 R$ 2,50 3 Borracha Escrita 3 R$ 1,75 4 Mochila Bolsas 2 R$ 89,90 5 Lápis Escrita 60 R$ 1,20 6 Papel A4 Papelaria 4 R$ 27,50 7 Estojo Bolsas 6 R$ 19,90 7 produtos cadastrados.
Repare nos preços: todos terminam na mesma coluna, porque cada um ocupa 10 posições. É o mesmo que a Ana faria no caderno, com uma régua.
A ficha não se apaga: records são imutáveis
A Ana vendeu um caderno e tentou diminuir a quantidade como faria numa variável comum:
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
];
estoque[0].Quantidade = 11; // vendeu um caderno
Console.WriteLine(estoque[0]);
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
$ dotnet run /home/ana/projetos/Estoque/Program.cs(6,1): error CS8852: A propriedade somente de inicialização ou o indexador 'Produto.Quantidade' só pode ser atribuído em um inicializador de objeto, em 'this' ou 'base' em um construtor de instância ou em um acessador 'init'. [/home/ana/projetos/Estoque/Estoque.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: as propriedades de um record posicional são init: recebem o valor quando a ficha é criada e depois não mudam mais (aula readonly e init). Em inglês, a mensagem começa com Init-only property or indexer 'Produto.Quantidade' can only be assigned….
Como consertar: em vez de rasurar a ficha, a Ana preenche uma ficha nova, igual à velha, com a quantidade trocada, e põe a nova no lugar. É isso que o with faz:
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
];
estoque[0] = estoque[0] with { Quantidade = 11 };
Console.WriteLine(estoque[0]);
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
$ dotnet run
Produto { Codigo = 1, Nome = Caderno, Categoria = Papelaria, Quantidade = 11, Preco = 15,90 }
De quebra, o Console.WriteLine de um record mostra todos os campos, com nome: ótimo para conferir o que está dentro.
Por que não usar uma class com set, que deixaria mudar? Daria certo também. O record foi escolhido porque, com LINQ, a maior parte do trabalho é ler o estoque, e uma ficha que ninguém muda por engano é mais segura. Só a versão 4 vai mudar quantidades, e sempre num lugar só.
Missão (versão 1): chegou um produto novo, a cola. Acrescente na lista o produto de código 8, nome "Cola", categoria "Escrita", 10 unidades a R$ 3,50 (lembre do m), e rode. A tabela deve ganhar uma linha e o total deve dizer 8 produtos cadastrados.
// Estoque da Lojinha — versão 1: a ficha e a lista
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
Console.WriteLine("=== Estoque da Lojinha ===");
Console.WriteLine("Cód Produto Categoria Qtd Preço");
foreach (Produto p in estoque)
{
Console.WriteLine($"{p.Codigo,3} {p.Nome,-10} {p.Categoria,-10} {p.Quantidade,3} {p.Preco,10:C}");
}
Console.WriteLine($"{estoque.Count} produtos cadastrados.");
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
// Estoque da Lojinha — versão 1: a ficha e a lista
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
new(8, "Cola", "Escrita", 10, 3.50m),
];
Console.WriteLine("=== Estoque da Lojinha ===");
Console.WriteLine("Cód Produto Categoria Qtd Preço");
foreach (Produto p in estoque)
{
Console.WriteLine($"{p.Codigo,3} {p.Nome,-10} {p.Categoria,-10} {p.Quantidade,3} {p.Preco,10:C}");
}
Console.WriteLine($"{estoque.Count} produtos cadastrados.");
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
Versão 2 · Perguntas ao estoque com LINQ
Com a lista pronta, chegam as perguntas da Ana. Sem LINQ, cada pergunta seria um foreach com um if e uma variável para ir somando. Com LINQ, cada pergunta vira uma frase: "do estoque, onde a quantidade é menor que 5"; "do estoque, a soma de quantidade vezes preço".
// Estoque da Lojinha — versão 2: perguntas com LINQ
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
// 1. Quais produtos estão acabando? (menos de 5 unidades)
var acabando = estoque.Where(p => p.Quantidade < 5);
Console.WriteLine("Comprar logo:");
foreach (Produto p in acabando)
{
Console.WriteLine($" {p.Nome} (só {p.Quantidade})");
}
// 2. Os nomes em ordem alfabética
var nomes = estoque.OrderBy(p => p.Nome).Select(p => p.Nome);
Console.WriteLine($"Em ordem: {string.Join(", ", nomes)}");
// 3. Contas sobre o estoque inteiro
int unidades = estoque.Sum(p => p.Quantidade);
decimal valor = estoque.Sum(p => p.Quantidade * p.Preco);
int caros = estoque.Count(p => p.Preco > 10m);
Produto maisCaro = estoque.MaxBy(p => p.Preco)!;
Console.WriteLine($"Unidades: {unidades}");
Console.WriteLine($"Valor do estoque: {valor:C}");
Console.WriteLine($"Produtos acima de R$ 10: {caros}");
Console.WriteLine($"Mais caro: {maisCaro.Nome}, {maisCaro.Preco:C}");
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
| Linha | O que faz |
|---|---|
estoque.Where(p => p.Quantidade < 5) | Filtra: fica só com os produtos em que a condição é verdadeira. O p => p.Quantidade < 5 é uma lambda: "dado um produto p, diga se a quantidade dele é menor que 5". Aula: Filtrar e Projetar. |
var acabando = …; | O resultado é um IEnumerable<Produto>, uma sequência que se percorre com foreach. O var poupa escrever esse nome comprido. |
.OrderBy(p => p.Nome) | Ordena pelo nome, de A a Z (aula Ordenar). A lista original continua na ordem de antes. |
.Select(p => p.Nome) | Projeta: troca cada produto por outra coisa, aqui só o nome. O resultado é uma sequência de string. |
string.Join(", ", nomes) | Junta os textos numa linha só, com ", " entre eles. |
estoque.Sum(p => p.Quantidade) | Soma o que a lambda devolve para cada produto: todas as unidades. |
estoque.Sum(p => p.Quantidade * p.Preco) | O valor do estoque: para cada produto, quantidade × preço, e a soma de tudo. int vezes decimal dá decimal, e a conta é exata. Aula: Agregar. |
estoque.Count(p => p.Preco > 10m) | Conta quantos produtos cumprem a condição. Sem lambda, Count() conta todos. |
estoque.MaxBy(p => p.Preco)! | Devolve o produto de maior preço (o Max devolveria só o preço). Numa lista vazia, ele devolveria null; o ! diz ao compilador "aqui não está vazia" e evita um aviso (aula Referências Anuláveis). .NET 6 |
$ dotnet run Comprar logo: Borracha (só 3) Mochila (só 2) Papel A4 (só 4) Em ordem: Borracha, Caderno, Caneta, Estojo, Lápis, Mochila, Papel A4 Unidades: 127 Valor do estoque: R$ 777,25 Produtos acima de R$ 10: 4 Mais caro: Mochila, R$ 89,90
Confira uma conta na mão: o caderno vale 12 × 15,90 = 190,80; a caneta, 40 × 2,50 = 100,00; somando os sete, dá R$ 777,25. Repare também no Lápis entre Estojo e Mochila: o OrderBy de texto segue as regras do português, e o á conta como a.
Nome
A Ana quis mostrar o nome do que está acabando direto do resultado do Where:
List<Produto> estoque =
[
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
];
var acabando = estoque.Where(p => p.Quantidade < 5);
Console.WriteLine(acabando.Nome);
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
$ dotnet run /home/ana/projetos/Estoque/Program.cs(8,28): error CS1061: ‘IEnumerable<Produto>’ não contém uma definição para "Nome" e não foi possível encontrar nenhum método de extensão "Nome" que aceite um primeiro argumento do tipo ‘IEnumerable<Produto>’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?) [/home/ana/projetos/Estoque/Estoque.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: o Where não devolve um produto, devolve uma sequência de produtos (podem ser zero, um ou vários). Uma sequência não tem nome; cada produto dentro dela, sim. A dica do fim (diretiva ou referência de assembly) não ajuda aqui: ela aparece em todo CS1061.
Como consertar: percorra com foreach, como na versão 2; ou projete os nomes: string.Join(", ", acabando.Select(p => p.Nome)); ou, se quer só o primeiro, acabando.First().Nome.
A consulta é uma receita, não uma foto
Um detalhe do LINQ que surpreende: a linha do Where não separa os produtos naquela hora. Ela guarda a "receita" do filtro, e a receita roda de novo a cada vez que alguém percorre o resultado. Veja o que acontece quando o estoque muda depois da consulta:
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
];
var acabando = estoque.Where(p => p.Quantidade < 5);
Console.WriteLine($"Acabando: {acabando.Count()}");
estoque[1] = estoque[1] with { Quantidade = 30 }; // chegou uma caixa de borrachas
Console.WriteLine($"Acabando: {acabando.Count()}");
var foto = estoque.Where(p => p.Quantidade < 5).ToList();
estoque[2] = estoque[2] with { Quantidade = 10 }; // chegaram mochilas
Console.WriteLine($"Acabando (consulta): {acabando.Count()}");
Console.WriteLine($"Acabando (ToList): {foto.Count}");
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
$ dotnet run Acabando: 2 Acabando: 1 Acabando (consulta): 0 Acabando (ToList): 1
A mesma variável acabando deu 2, depois 1, depois 0, porque cada Count() refez o filtro na lista do momento. Isso se chama execução adiada. Quando você quer guardar o resultado daquele momento, termine a consulta com .ToList(): ela roda na hora e copia os produtos para uma lista nova. Aula: O que é LINQ.
Missão (versão 2): a Ana quer saber quanto vale só a Papelaria. Troque o 0m por uma consulta: primeiro um Where que fica com a categoria "Papelaria", depois um Sum de quantidade × preço. O resultado certo é Papelaria vale R$ 300,80.
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
decimal total = estoque.Sum(p => p.Quantidade * p.Preco);
Console.WriteLine($"Estoque inteiro: {total:C}");
// Troque o 0m por uma consulta: Where (só a Papelaria) e depois Sum
decimal papelaria = 0m;
Console.WriteLine($"Papelaria vale {papelaria:C}");
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
decimal total = estoque.Sum(p => p.Quantidade * p.Preco);
Console.WriteLine($"Estoque inteiro: {total:C}");
// Troque o 0m por uma consulta: Where (só a Papelaria) e depois Sum
decimal papelaria = estoque.Where(p => p.Categoria == "Papelaria").Sum(p => p.Quantidade * p.Preco);
Console.WriteLine($"Papelaria vale {papelaria:C}");
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
Versão 3 · O relatório por categoria e a busca
Agora a Ana quer o relatório do fim do mês: para cada categoria, quantos produtos, quantas unidades e quanto vale. No caderno, ela faria três pilhas de fichas (Papelaria, Escrita, Bolsas) e somaria cada pilha. O GroupBy ("agrupar por") faz exatamente as pilhas.
// Estoque da Lojinha — versão 3: relatório por categoria e busca
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
// Relatório: uma linha por categoria, da que vale mais para a que vale menos
var relatorio = estoque
.GroupBy(p => p.Categoria)
.Select(g => new
{
Categoria = g.Key,
Produtos = g.Count(),
Unidades = g.Sum(p => p.Quantidade),
Valor = g.Sum(p => p.Quantidade * p.Preco)
})
.OrderByDescending(c => c.Valor);
Console.WriteLine("Categoria Produtos Unidades Valor");
foreach (var c in relatorio)
{
Console.WriteLine($"{c.Categoria,-10} {c.Produtos,8} {c.Unidades,8} {c.Valor,10:C}");
}
// Busca pelo nome, sem ligar para maiúsculas
Mostrar(Buscar(estoque, "caneta"));
Mostrar(Buscar(estoque, "régua"));
bool temBolsa = estoque.Any(p => p.Categoria == "Bolsas");
bool tudoTem = estoque.All(p => p.Quantidade > 0);
Console.WriteLine($"Vende bolsas? {temBolsa}. Nada esgotado? {tudoTem}.");
static Produto? Buscar(List<Produto> lista, string nome) =>
lista.FirstOrDefault(p => p.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase));
static void Mostrar(Produto? p)
{
if (p is null)
{
Console.WriteLine("Não temos esse produto.");
return;
}
Console.WriteLine($"{p.Nome}: {p.Quantidade} unidades a {p.Preco:C}");
}
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
| Linha | O que faz |
|---|---|
.GroupBy(p => p.Categoria) | Faz uma pilha para cada categoria diferente. Cada pilha g tem uma chave, g.Key (o nome da categoria), e é ela mesma uma sequência com os produtos daquela categoria. Aula: Agrupar. |
.Select(g => new { Categoria = g.Key, … }) | Troca cada pilha por um resumo dela. O new { … } sem nome de tipo cria um tipo anônimo: um objeto só com esses campos, que serve só para este relatório. |
g.Count(), g.Sum(p => p.Quantidade) | As mesmas contas da versão 2, mas só com os produtos da pilha. |
.OrderByDescending(c => c.Valor) | Ordena do maior valor para o menor ("descending" = decrescente). |
foreach (var c in relatorio) | Aqui o var é obrigatório: o tipo anônimo não tem nome para escrever. |
static Produto? Buscar(…) => lista.FirstOrDefault(…) | Uma função local que devolve o primeiro produto que combina, ou null se nenhum combina (o "OrDefault"). O ? em Produto? avisa que pode vir null. |
p.Nome.Equals(nome, StringComparison.OrdinalIgnoreCase) | Compara ignorando maiúsculas e minúsculas: "caneta" acha "Caneta" (aula Métodos de String). |
if (p is null) | Antes de usar o resultado de FirstOrDefault, confira se veio alguma coisa (aula null). |
estoque.Any(…), estoque.All(…) | Any: "algum produto cumpre?". All: "todos cumprem?". Os dois devolvem bool, que o Console mostra como True ou False. |
$ dotnet run Categoria Produtos Unidades Valor Papelaria 2 16 R$ 300,80 Bolsas 2 8 R$ 299,20 Escrita 3 103 R$ 177,25 Caneta: 40 unidades a R$ 2,50 Não temos esse produto. Vende bolsas? True. Nada esgotado? True.
A Escrita tem quase todas as unidades (103 de 127), mas é a que vale menos: são canetas e lápis baratos. A Papelaria, com só 16 unidades, vale mais. É o tipo de coisa que o relatório mostra e o olho não vê.
First sem resultado: InvalidOperationException
Na primeira busca, a Ana usou First em vez de FirstOrDefault, e procurou um produto que a loja não tem:
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
];
Produto regua = estoque.First(p => p.Nome == "Régua");
Console.WriteLine(regua.Preco);
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
$ dotnet run Unhandled exception. System.InvalidOperationException: Sequence contains no matching element at System.Linq.ThrowHelper.ThrowNoMatchException() at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate) at Program.<Main>$(String[] args) in /home/ana/projetos/Estoque/Program.cs:line 7 $ echo $? 134
O que significa: "a sequência não contém nenhum elemento que combine". O First promete devolver um produto; quando não há nenhum, ele não tem o que devolver e lança uma exceção. O programa cai na linha 7, com o código de saída 134.
Como consertar: quando pode não haver resultado (busca pelo que o usuário digitou, por exemplo), use FirstOrDefault e confira o null, como na versão 3. Deixe o First para quando "não achar" seria mesmo um defeito do programa.
Missão (versão 3): este programa deveria mostrar uma linha por categoria, com os nomes dos produtos dela, mas mostra uma linha por produto. O defeito está na chave do GroupBy. Conserte para a saída ter Escrita: Caneta, Borracha, Lápis.
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
// Uma linha por categoria, com os nomes dos produtos dela
var grupos = estoque.GroupBy(p => p.Nome);
foreach (var g in grupos)
{
Console.WriteLine($"{g.Key}: {string.Join(", ", g.Select(p => p.Nome))}");
}
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
// Uma linha por categoria, com os nomes dos produtos dela
var grupos = estoque.GroupBy(p => p.Categoria);
foreach (var g in grupos)
{
Console.WriteLine($"{g.Key}: {string.Join(", ", g.Select(p => p.Nome))}");
}
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
Versão 4 · O menu: entrada e saída
Até aqui, o programa só olha. Agora ele vai mudar o estoque: quando chega mercadoria (entrada) e quando a Ana vende (saída). Um menu pergunta o que fazer, até ela digitar 0. As consultas da versão 2 continuam úteis: a opção "Acabando" é o mesmo Where, passado para a função que lista.
// Estoque da Lojinha — versão 4: o menu, a entrada e a saída
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
Console.WriteLine("=== Estoque da Lojinha ===");
while (true)
{
Console.WriteLine();
Console.WriteLine("1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair");
string opcao = Perguntar("Opção: ");
if (opcao == "0")
{
break;
}
switch (opcao)
{
case "1": Listar(estoque); break;
case "2": Listar(estoque.Where(p => p.Quantidade < 5)); break;
case "3": Movimentar(estoque, +1); break;
case "4": Movimentar(estoque, -1); break;
default: Console.WriteLine("Opção inválida."); break;
}
}
Console.WriteLine($"Até amanhã! Valor do estoque: {estoque.Sum(p => p.Quantidade * p.Preco):C}");
static string Perguntar(string texto)
{
Console.Write(texto);
return Console.ReadLine()?.Trim() ?? "0"; // fim da entrada = sair
}
static void Listar(IEnumerable<Produto> produtos)
{
Console.WriteLine("Cód Produto Qtd Preço");
foreach (Produto p in produtos)
{
Console.WriteLine($"{p.Codigo,3} {p.Nome,-10} {p.Quantidade,3} {p.Preco,10:C}");
}
}
// sinal = +1 (chegou mercadoria) ou -1 (vendeu)
static void Movimentar(List<Produto> lista, int sinal)
{
if (!int.TryParse(Perguntar("Código: "), out int codigo))
{
Console.WriteLine("Código inválido.");
return;
}
int i = lista.FindIndex(p => p.Codigo == codigo);
if (i == -1)
{
Console.WriteLine($"Não há produto com o código {codigo}.");
return;
}
if (!int.TryParse(Perguntar("Quantas unidades? "), out int qtd) || qtd <= 0)
{
Console.WriteLine("Quantidade inválida.");
return;
}
Produto p = lista[i];
if (sinal < 0 && qtd > p.Quantidade)
{
Console.WriteLine($"Só há {p.Quantidade} de {p.Nome}.");
return;
}
lista[i] = p with { Quantidade = p.Quantidade + sinal * qtd };
Console.WriteLine($"{p.Nome}: agora são {lista[i].Quantidade}.");
}
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
| Linha | O que faz |
|---|---|
while (true) { … break; } | Repete o menu para sempre; o break sai do laço quando a opção é "0" (aulas while e break). |
switch (opcao) { case "1": … } | Escolhe o que fazer pelo texto digitado. O default pega tudo o que não é opção (aula switch). |
Console.ReadLine()?.Trim() ?? "0" | Lê uma linha. Quando a entrada acaba (o usuário apertou Ctrl+D), o ReadLine devolve null: o ?. não chama o Trim, e o ?? troca o null por "0", que sai do programa (aula Operadores ??, ?. e ??=). |
static void Listar(IEnumerable<Produto> produtos) | O parâmetro é IEnumerable, e não List: assim, a mesma função lista a List inteira e o resultado de um Where (aula IEnumerable e foreach). |
int.TryParse(…, out int codigo) | Tenta converter o texto em número. Devolve false se a Ana digitou abc, em vez de derrubar o programa como o int.Parse faria (aula Parse e TryParse). |
lista.FindIndex(p => p.Codigo == codigo) | A posição do produto na lista, ou -1 se não há. É um método da própria List, não do LINQ: precisamos da posição para trocar a ficha. |
if (sinal < 0 && qtd > p.Quantidade) | Na saída, não deixa vender mais do que há. |
lista[i] = p with { Quantidade = … }; | Uma ficha nova, cópia da velha com a quantidade nova, no mesmo lugar da lista. sinal * qtd soma na entrada e subtrai na saída. |
A Ana rodou e fez uma tarde de trabalho (os números depois de Opção:, Código: e Quantas unidades? são o que ela digitou):
$ dotnet run === Estoque da Lojinha === 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 2 Cód Produto Qtd Preço 3 Borracha 3 R$ 1,75 4 Mochila 2 R$ 89,90 6 Papel A4 4 R$ 27,50 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 3 Código: 3 Quantas unidades? 20 Borracha: agora são 23. 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 4 Código: 4 Quantas unidades? 3 Só há 2 de Mochila. 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 4 Código: abc Código inválido. 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 4 Código: 2 Quantas unidades? 5 Caneta: agora são 35. 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 1 Cód Produto Qtd Preço 1 Caderno 12 R$ 15,90 2 Caneta 35 R$ 2,50 3 Borracha 23 R$ 1,75 4 Mochila 2 R$ 89,90 5 Lápis 60 R$ 1,20 6 Papel A4 4 R$ 27,50 7 Estojo 6 R$ 19,90 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 0 Até amanhã! Valor do estoque: R$ 799,75
foreach: "Collection was modified"
A Ana pensou numa opção para tirar da lista os produtos esgotados e escreveu o jeito mais direto:
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(4, "Mochila", "Bolsas", 0, 89.90m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
// tirar da lista o que esgotou
foreach (Produto p in estoque)
{
if (p.Quantidade == 0)
{
estoque.Remove(p);
}
}
Console.WriteLine(estoque.Count);
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
$ dotnet run Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute. at System.Collections.Generic.List`1.Enumerator.MoveNext() at Program.<Main>$(String[] args) in /home/ana/projetos/Estoque/Program.cs:line 9
O que significa: "a coleção foi modificada; a enumeração não pode continuar". O foreach vai andando pela lista; quando ela muda no meio do caminho, ele não sabe mais onde está e para com uma exceção (na linha 9, a do foreach).
Como consertar: não mexa na lista que o foreach está percorrendo. Para apagar, a List tem um método pronto, que faz o laço por dentro: estoque.RemoveAll(p => p.Quantidade == 0);. Ou, no estilo LINQ, crie uma lista nova só com o que fica: estoque = estoque.Where(p => p.Quantidade > 0).ToList();. O mesmo vale para Add dentro do foreach.
Missão (versão 4): a função Movimentar está sem o teste da saída, e a Ana consegue vender 3 mochilas quando só tem 2 (o estoque fica em -1). Escreva o teste no lugar do comentário. A entrada já vem pronta: opção 4 (saída), código 4, 3 unidades; depois lista (1) e sai (0). Com o teste certo, aparece Só há 2 de Mochila. e o valor final continua R$ 777,25.
// Estoque da Lojinha — versão 4: o menu, a entrada e a saída
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
Console.WriteLine("=== Estoque da Lojinha ===");
while (true)
{
Console.WriteLine();
Console.WriteLine("1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair");
string opcao = Perguntar("Opção: ");
if (opcao == "0")
{
break;
}
switch (opcao)
{
case "1": Listar(estoque); break;
case "2": Listar(estoque.Where(p => p.Quantidade < 5)); break;
case "3": Movimentar(estoque, +1); break;
case "4": Movimentar(estoque, -1); break;
default: Console.WriteLine("Opção inválida."); break;
}
}
Console.WriteLine($"Até amanhã! Valor do estoque: {estoque.Sum(p => p.Quantidade * p.Preco):C}");
static string Perguntar(string texto)
{
Console.Write(texto);
return Console.ReadLine()?.Trim() ?? "0"; // fim da entrada = sair
}
static void Listar(IEnumerable<Produto> produtos)
{
Console.WriteLine("Cód Produto Qtd Preço");
foreach (Produto p in produtos)
{
Console.WriteLine($"{p.Codigo,3} {p.Nome,-10} {p.Quantidade,3} {p.Preco,10:C}");
}
}
// sinal = +1 (chegou mercadoria) ou -1 (vendeu)
static void Movimentar(List<Produto> lista, int sinal)
{
if (!int.TryParse(Perguntar("Código: "), out int codigo))
{
Console.WriteLine("Código inválido.");
return;
}
int i = lista.FindIndex(p => p.Codigo == codigo);
if (i == -1)
{
Console.WriteLine($"Não há produto com o código {codigo}.");
return;
}
if (!int.TryParse(Perguntar("Quantas unidades? "), out int qtd) || qtd <= 0)
{
Console.WriteLine("Quantidade inválida.");
return;
}
Produto p = lista[i];
// FALTA AQUI: na saída (sinal < 0), não deixe vender mais do que há.
// Mostre "Só há 2 de Mochila." (com p.Quantidade e p.Nome) e saia com return.
lista[i] = p with { Quantidade = p.Quantidade + sinal * qtd };
Console.WriteLine($"{p.Nome}: agora são {lista[i].Quantidade}.");
}
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
// Estoque da Lojinha — versão 4: o menu, a entrada e a saída
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
Console.WriteLine("=== Estoque da Lojinha ===");
while (true)
{
Console.WriteLine();
Console.WriteLine("1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair");
string opcao = Perguntar("Opção: ");
if (opcao == "0")
{
break;
}
switch (opcao)
{
case "1": Listar(estoque); break;
case "2": Listar(estoque.Where(p => p.Quantidade < 5)); break;
case "3": Movimentar(estoque, +1); break;
case "4": Movimentar(estoque, -1); break;
default: Console.WriteLine("Opção inválida."); break;
}
}
Console.WriteLine($"Até amanhã! Valor do estoque: {estoque.Sum(p => p.Quantidade * p.Preco):C}");
static string Perguntar(string texto)
{
Console.Write(texto);
return Console.ReadLine()?.Trim() ?? "0"; // fim da entrada = sair
}
static void Listar(IEnumerable<Produto> produtos)
{
Console.WriteLine("Cód Produto Qtd Preço");
foreach (Produto p in produtos)
{
Console.WriteLine($"{p.Codigo,3} {p.Nome,-10} {p.Quantidade,3} {p.Preco,10:C}");
}
}
// sinal = +1 (chegou mercadoria) ou -1 (vendeu)
static void Movimentar(List<Produto> lista, int sinal)
{
if (!int.TryParse(Perguntar("Código: "), out int codigo))
{
Console.WriteLine("Código inválido.");
return;
}
int i = lista.FindIndex(p => p.Codigo == codigo);
if (i == -1)
{
Console.WriteLine($"Não há produto com o código {codigo}.");
return;
}
if (!int.TryParse(Perguntar("Quantas unidades? "), out int qtd) || qtd <= 0)
{
Console.WriteLine("Quantidade inválida.");
return;
}
Produto p = lista[i];
if (sinal < 0 && qtd > p.Quantidade)
{
Console.WriteLine($"Só há {p.Quantidade} de {p.Nome}.");
return;
}
lista[i] = p with { Quantidade = p.Quantidade + sinal * qtd };
Console.WriteLine($"{p.Nome}: agora são {lista[i].Quantidade}.");
}
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
Versão 5 · O estoque guardado num arquivo
A versão 4 tem um problema grave: ao sair, tudo o que a Ana fez some, e no dia seguinte a caneta volta a ter 40. A memória do programa acaba junto com ele. Para lembrar, o estoque vai para um arquivo de texto, estoque.csv, uma linha por produto, com os campos separados por ;. O formato CSV ("valores separados por vírgula") abre até numa planilha.
Antes: o centavo que virou milhar
A primeira tentativa da Ana usou vírgula entre os campos e escreveu o preço do jeito normal, com {p.Preco}:
// errado.cs — a primeira tentativa da Ana de salvar em CSV
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
];
// salvar: uma linha por produto, campos separados por vírgula
File.WriteAllLines("estoque.csv",
estoque.Select(p => $"{p.Codigo},{p.Nome},{p.Categoria},{p.Quantidade},{p.Preco}"));
// carregar de volta
List<Produto> lido = File.ReadLines("estoque.csv")
.Select(linha => linha.Split(','))
.Select(c => new Produto(int.Parse(c[0]), c[1], c[2], int.Parse(c[3]), decimal.Parse(c[4])))
.ToList();
foreach (Produto p in lido)
{
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
}
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
$ dotnet run Caderno: R$ 15,00 Caneta: R$ 2,00 $ cat estoque.csv 1,Caderno,Papelaria,12,15,90 2,Caneta,Escrita,40,2,50
O que aconteceu: na máquina da Ana, em pt-BR, 15.90m vira o texto 15,90, com vírgula. A linha do caderno ficou com seis pedaços, e não cinco: …,12,15,90. Na leitura, o Split(',') separou o preço em "15" e "90"; o programa leu só o "15", e os centavos sumiram. Não houve erro nenhum, e esse é o pior tipo de defeito.
Como consertar: duas mudanças, e a versão 5 usa as duas. Separe os campos com ;, que não aparece em números. E grave e leia os números sempre do mesmo jeito, independente do país do computador, com CultureInfo.InvariantCulture (a "cultura invariante", que usa ponto): o preço vai para o arquivo como 15.90 (aula Cultura).
A versão 5 é a 4 com duas funções novas, Carregar e Salvar, e duas linhas novas no começo e no fim:
// Estoque da Lojinha — versão 5: o estoque guardado em estoque.csv
using System.Globalization;
const string Arquivo = "estoque.csv";
List<Produto> estoque = Carregar();
Console.WriteLine("=== Estoque da Lojinha ===");
Console.WriteLine($"{estoque.Count} produtos carregados.");
while (true)
{
Console.WriteLine();
Console.WriteLine("1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair");
string opcao = Perguntar("Opção: ");
if (opcao == "0")
{
break;
}
switch (opcao)
{
case "1": Listar(estoque); break;
case "2": Listar(estoque.Where(p => p.Quantidade < 5)); break;
case "3": Movimentar(estoque, +1); break;
case "4": Movimentar(estoque, -1); break;
default: Console.WriteLine("Opção inválida."); break;
}
}
Salvar(estoque);
Console.WriteLine($"Salvei {estoque.Count} produtos em {Arquivo}. Até amanhã!");
static string Perguntar(string texto)
{
Console.Write(texto);
return Console.ReadLine()?.Trim() ?? "0"; // fim da entrada = sair
}
static void Listar(IEnumerable<Produto> produtos)
{
Console.WriteLine("Cód Produto Qtd Preço");
foreach (Produto p in produtos)
{
Console.WriteLine($"{p.Codigo,3} {p.Nome,-10} {p.Quantidade,3} {p.Preco,10:C}");
}
}
// sinal = +1 (chegou mercadoria) ou -1 (vendeu)
static void Movimentar(List<Produto> lista, int sinal)
{
if (!int.TryParse(Perguntar("Código: "), out int codigo))
{
Console.WriteLine("Código inválido.");
return;
}
int i = lista.FindIndex(p => p.Codigo == codigo);
if (i == -1)
{
Console.WriteLine($"Não há produto com o código {codigo}.");
return;
}
if (!int.TryParse(Perguntar("Quantas unidades? "), out int qtd) || qtd <= 0)
{
Console.WriteLine("Quantidade inválida.");
return;
}
Produto p = lista[i];
if (sinal < 0 && qtd > p.Quantidade)
{
Console.WriteLine($"Só há {p.Quantidade} de {p.Nome}.");
return;
}
lista[i] = p with { Quantidade = p.Quantidade + sinal * qtd };
Console.WriteLine($"{p.Nome}: agora são {lista[i].Quantidade}.");
}
static List<Produto> Carregar()
{
if (!File.Exists(Arquivo))
{
Console.WriteLine($"{Arquivo} não existe: começando do caderno de papel.");
return
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
new(3, "Borracha", "Escrita", 3, 1.75m),
new(4, "Mochila", "Bolsas", 2, 89.90m),
new(5, "Lápis", "Escrita", 60, 1.20m),
new(6, "Papel A4", "Papelaria", 4, 27.50m),
new(7, "Estojo", "Bolsas", 6, 19.90m),
];
}
return File.ReadLines(Arquivo)
.Skip(1) // pula o cabeçalho
.Where(linha => linha.Trim() != "") // ignora linhas vazias
.Select(LerLinha)
.ToList();
}
static Produto LerLinha(string linha)
{
string[] c = linha.Split(';');
return new Produto(int.Parse(c[0]), c[1], c[2], int.Parse(c[3]),
decimal.Parse(c[4], CultureInfo.InvariantCulture));
}
static void Salvar(List<Produto> lista)
{
var linhas = lista.Select(p => string.Join(";", p.Codigo, p.Nome, p.Categoria, p.Quantidade,
p.Preco.ToString(CultureInfo.InvariantCulture)));
File.WriteAllLines(Arquivo, linhas.Prepend("codigo;nome;categoria;quantidade;preco"));
}
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
| Linha | O que faz |
|---|---|
using System.Globalization; | Traz o CultureInfo. Fica na primeira linha do arquivo (aula Namespaces e using). |
const string Arquivo = "estoque.csv"; | O nome do arquivo num lugar só. Por ser const, as funções static podem usá-lo (aula Constantes). O arquivo fica na pasta em que a Ana roda o programa. |
if (!File.Exists(Arquivo)) | Na primeira vez, o arquivo não existe, e o programa começa com os produtos do caderno de papel (aula Arquivos). |
File.ReadLines(Arquivo) | Lê o arquivo linha por linha, como uma sequência de string. Por ser uma sequência, aceita LINQ. |
.Skip(1) | Pula a primeira linha, o cabeçalho codigo;nome;…. |
.Where(linha => linha.Trim() != "") | Ignora linhas em branco (um Enter a mais no fim do arquivo, por exemplo). |
.Select(LerLinha) | Transforma cada linha num Produto. Passar o nome do método é o mesmo que .Select(linha => LerLinha(linha)). |
.ToList() | Roda a consulta na hora e devolve a List que o menu vai mudar. |
linha.Split(';') | Corta a linha em pedaços no ;: "1;Caderno;Papelaria;12;15.90" vira um array de 5 textos. |
decimal.Parse(c[4], CultureInfo.InvariantCulture) | Lê "15.90" com ponto, do jeito que foi gravado, em qualquer computador. |
string.Join(";", p.Codigo, p.Nome, …) | Monta a linha com ; entre os campos. |
p.Preco.ToString(CultureInfo.InvariantCulture) | Escreve o preço com ponto: 15.90. |
linhas.Prepend("codigo;nome;…") | Põe o cabeçalho antes das linhas dos produtos. |
File.WriteAllLines(Arquivo, …) | Cria o arquivo (ou apaga o que havia) e grava uma linha para cada texto da sequência. |
Na primeira vez, o arquivo não existe. A Ana faz a tarde de trabalho do começo da página (O que vamos construir) e sai; o programa grava o arquivo. No dia seguinte, ela olha o arquivo e roda de novo:
$ cat estoque.csv codigo;nome;categoria;quantidade;preco 1;Caderno;Papelaria;12;15.90 2;Caneta;Escrita;35;2.50 3;Borracha;Escrita;23;1.75 4;Mochila;Bolsas;2;89.90 5;Lápis;Escrita;60;1.20 6;Papel A4;Papelaria;4;27.50 7;Estojo;Bolsas;6;19.90 $ dotnet run === Estoque da Lojinha === 7 produtos carregados. 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 1 Cód Produto Qtd Preço 1 Caderno 12 R$ 15,90 2 Caneta 35 R$ 2,50 « lembrou da venda de ontem 3 Borracha 23 R$ 1,75 « e da entrada 4 Mochila 2 R$ 89,90 5 Lápis 60 R$ 1,20 6 Papel A4 4 R$ 27,50 7 Estojo 6 R$ 19,90 1) Listar 2) Acabando 3) Entrada 4) Saída 0) Sair Opção: 0 Salvei 7 produtos em estoque.csv. Até amanhã!
Desta vez não apareceu a linha estoque.csv não existe: os 7 produtos vieram do arquivo. No arquivo, os preços estão com ponto (15.90); na tela, com vírgula (R$ 15,90). O arquivo é para o programa; a tela é para a Ana.
Missão (versão 5): este programa grava dois produtos em estoque.csv, mostra o arquivo e lê de volta. A leitura já usa InvariantCulture, mas a gravação não: o preço vai para o arquivo como 15,90, e a leitura invariante entende a vírgula como separador de milhar (o caderno sai a R$ 1.590,00!). Conserte a gravação para o preço ir com ponto, e a saída mostrar Caderno: R$ 15,90.
using System.Globalization;
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
];
// Salvar: o preço tem de ir com ponto, do jeito "invariante"
File.WriteAllLines("estoque.csv",
estoque.Select(p => $"{p.Codigo};{p.Nome};{p.Categoria};{p.Quantidade};{p.Preco}"));
// Carregar: lê o preço com ponto (InvariantCulture)
List<Produto> lido = File.ReadLines("estoque.csv")
.Select(linha => linha.Split(';'))
.Select(c => new Produto(int.Parse(c[0]), c[1], c[2], int.Parse(c[3]),
decimal.Parse(c[4], CultureInfo.InvariantCulture)))
.ToList();
Console.WriteLine(File.ReadAllText("estoque.csv"));
foreach (Produto p in lido)
{
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
}
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
using System.Globalization;
List<Produto> estoque =
[
new(1, "Caderno", "Papelaria", 12, 15.90m),
new(2, "Caneta", "Escrita", 40, 2.50m),
];
// Salvar: o preço tem de ir com ponto, do jeito "invariante"
File.WriteAllLines("estoque.csv",
estoque.Select(p => $"{p.Codigo};{p.Nome};{p.Categoria};{p.Quantidade};{p.Preco.ToString(CultureInfo.InvariantCulture)}"));
// Carregar: lê o preço com ponto (InvariantCulture)
List<Produto> lido = File.ReadLines("estoque.csv")
.Select(linha => linha.Split(';'))
.Select(c => new Produto(int.Parse(c[0]), c[1], c[2], int.Parse(c[3]),
decimal.Parse(c[4], CultureInfo.InvariantCulture)))
.ToList();
Console.WriteLine(File.ReadAllText("estoque.csv"));
foreach (Produto p in lido)
{
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
}
record Produto(int Codigo, string Nome, string Categoria, int Quantidade, decimal Preco);
Formas diferentes
| Em vez de… | Daria para… | Por que o projeto não fez |
|---|---|---|
estoque.Where(p => p.Quantidade < 5) | Sintaxe de consulta: from p in estoque where p.Quantidade < 5 select p | É o mesmo programa depois de compilado. A forma com métodos é a mais usada e tem todos os operadores (MaxBy, Prepend… não existem na sintaxe de consulta). Veja O que é LINQ. |
record Produto(…) e with | class Produto com { get; set; } e p.Quantidade -= qtd | Funciona. O record impede mudanças por engano e já mostra os campos no WriteLine. O projeto Lista de Tarefas usa uma classe com set. |
estoque.csv com ; | estoque.json com System.Text.Json | O JSON resolve as vírgulas e a cultura sozinho; o projeto Agenda usa JSON. O CSV foi escolhido aqui porque se lê com LINQ, linha por linha, e abre numa planilha. |
FindIndex + lista[i] = … | Dictionary<int, Produto> com o código como chave | Com milhares de produtos, o dicionário acha o código mais rápido (aula Dictionary). Para sete, a lista é mais simples e mantém a ordem. |
Desafios extras
- Cadastrar: uma opção 5 que pede nome, categoria, quantidade e preço e acrescenta o produto com o código seguinte:
estoque.Max(p => p.Codigo) + 1. Leia o preço comdecimal.TryParse(em pt-BR, a Ana digita3,50). - Relatório no menu: leve o
GroupByda versão 3 para uma opção 6. - Arquivo estragado: abra o
estoque.csvnum editor, troque um preço porabce rode. Odecimal.ParselançaFormatException. Troque porTryParsee pule a linha com um aviso. - Busca por pedaço do nome:
p.Nome.Contains(texto, StringComparison.OrdinalIgnoreCase)acha "Papel A4" digitando "papel".
Exercícios
1. O que estoque.Where(p => p.Quantidade < 5) devolve?
2. A Ana procura um produto pelo nome que o cliente falou. Qual método é o certo?
3. Por que estoque[0].Quantidade = 11; não compila neste projeto?
4. Na máquina da Ana, $"{15.90m}" vira 15,90. O que isso causa num CSV separado por vírgula?
5. Complete para somar o valor da categoria Bolsas:
6. Complete para trocar a ficha da posição i por uma cópia com 5 unidades a menos:
7. Complete para pular o cabeçalho do arquivo antes de ler os produtos:
Resumo
- Um
recorddescreve a ficha; umaList<Produto>guarda todas. Para "mudar" um record, crie uma cópia comwith. Wherefiltra,OrderByordena,Selecttransforma,CounteSumfazem contas,MaxByacha o maior,AnyeAllrespondem sim ou não.GroupByfaz uma pilha por chave (g.Key); cada pilha aceita as mesmas contas.Firstlança exceção se não acha;FirstOrDefaultdevolvenull, e você confere.- Uma consulta LINQ roda de novo a cada uso;
ToList()tira uma "foto" na hora. - Não mude a lista dentro do
foreachque a percorre: useRemoveAllou crie uma lista nova. - Em arquivos, grave e leia números com
CultureInfo.InvariantCulture, e separe campos com um caractere que não aparece nos dados.