Projeto Boletim de Notas
A Ana dá aula de reforço para uma turma pequena e quer um programa que faça o boletim: ela digita o nome e a nota de cada aluno, e o programa mostra a média da turma, quem tirou a maior e a menor nota, quantos passaram e uma tabela bem alinhada. É o projeto para treinar arrays (muitos valores numa variável só) e records (os dados de um aluno juntos, num tipo só).
- Guardar as notas num array e percorrê-lo com for e foreach.
- Somar, tirar a média e mostrar com
:N1(Formatar Números). - Juntar nome e nota num record
Alunoe fazer um array de alunos. - Achar o maior, o menor e contar quem passou, num laço só.
- Validar a entrada num método
LerNota. - Decidir a situação com uma expressão switch e alinhar a tabela com
{nome,-10}.
O que vamos construir
O boletim pronto, na máquina da Ana, com cinco alunos:
$ dotnet run boletim.cs === Boletim da Turma === Quantos alunos (1 a 30)? 5 Nome do aluno 1: Bruno Nota de Bruno (0 a 10): 7,5 Nome do aluno 2: Carla Nota de Carla (0 a 10): 9 Nome do aluno 3: Diego Nota de Diego (0 a 10): 5,5 Nome do aluno 4: Elisa Nota de Elisa (0 a 10): 8 Nome do aluno 5: Gabriel Nota de Gabriel (0 a 10): 3,5 Aluno Nota Situação ---------- ----- ----------- Bruno 7,5 Aprovado Carla 9,0 Aprovado Diego 5,5 Recuperação Elisa 8,0 Aprovado Gabriel 3,5 Reprovado ---------- ----- ----------- Média 6,7 Maior nota: 9,0 (Carla) Menor nota: 3,5 (Gabriel) Aprovados: 3 de 5
- As notas num array e a média.
- Nome e nota juntos: um array de records.
- Maior, menor e aprovados.
- A tabela alinhada (quantidade de alunos,
LerNotae a expressão switch).
Antes de começar
Você precisa ter visto Arrays, for, foreach e Ler do Teclado; para a versão 2, Records; para a 4, Métodos e Expressões switch. É um arquivo só, boletim.cs:
$ mkdir -p cs/boletim $ cd cs/boletim
Versão 1 · As notas num array e a média
Cinco alunos, cinco notas. Daria para criar nota1, nota2… nota5, mas e se a turma tiver 30? Um array guarda todas numa variável só: notas[0] é a primeira, notas[4] a quinta. E um for passa por todas, sem repetir código.
const int Quantidade = 5;
double[] notas = new double[Quantidade];
double soma = 0;
Console.WriteLine("=== Boletim da Turma ===");
for (int i = 0; i < notas.Length; i++)
{
Console.Write($"Nota do aluno {i + 1}: ");
notas[i] = double.Parse(Console.ReadLine()!);
soma += notas[i];
}
Console.WriteLine($"Notas: {string.Join(" | ", notas)}");
Console.WriteLine($"Média da turma: {soma / notas.Length:N1}");
| Linha | O que faz |
|---|---|
const int Quantidade = 5; | O tamanho da turma, com nome. |
double[] notas = new double[Quantidade]; | Cria um array de 5 double. Um array novo de números já vem com todos os valores em 0. |
for (int i = 0; i < notas.Length; i++) | i vale 0, 1, 2, 3 e 4: as posições do array. notas.Length é o tamanho (5). Repare no <: a posição 5 não existe (veja o cenário abaixo). |
$"Nota do aluno {i + 1}: " | Para a Ana, o primeiro aluno é o 1, não o 0. Por isso mostramos i + 1. |
notas[i] = double.Parse(…); | Guarda a nota lida na posição i. |
soma += notas[i]; | Acumula: soma = soma + notas[i] (Operadores de Atribuição). |
string.Join(" | ", notas) | Junta todas as notas num texto, com " | " entre elas. (Uma vírgula como separador confundiria com a vírgula decimal: 7,5, 9, 5,5.) |
{soma / notas.Length:N1} | A média com 1 casa decimal (N1), no formato brasileiro. |
$ dotnet run boletim.cs === Boletim da Turma === Nota do aluno 1: 7,5 Nota do aluno 2: 9 Nota do aluno 3: 5,5 Nota do aluno 4: 8 Nota do aluno 5: 3,5 Notas: 7,5 | 9 | 5,5 | 8 | 3,5 Média da turma: 6,7
i <= notas.Length: IndexOutOfRangeException
A Ana escreveu for (int i = 0; i <= notas.Length; i++), com <=:
$ dotnet run boletim.cs === Boletim da Turma === Nota do aluno 1: 7,5 Nota do aluno 2: 9 Nota do aluno 3: 5,5 Nota do aluno 4: 8 Nota do aluno 5: 3,5 Nota do aluno 6: 10 « pediu uma nota a mais Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array. at Program.<Main>$(String[] args) in /home/ana/cs/boletim/boletim.cs:line 10 $ echo $? 134
O que significa: "o índice ficou fora dos limites do array". Com <=, o laço também roda com i igual a 5, e a linha 10 tenta guardar a sexta nota em notas[5], uma posição que não existe. O C# confere todo acesso a um array e, em vez de estragar a memória ao lado (como o C faria), lança a exceção na hora, com a linha exata.
Como consertar: um array de 5 posições vai de 0 a 4; o laço é i < notas.Length. Ou use foreach, que nunca passa do fim.
Missão (versão 1): o programa lê as notas, mas a média sai 0,0. Dentro do laço, some cada nota em soma com soma += notas[i];. A entrada da missão é 7,5, 9, 5,5, 8 e 3,5.
const int Quantidade = 5;
double[] notas = new double[Quantidade];
double soma = 0;
Console.WriteLine("=== Boletim da Turma ===");
for (int i = 0; i < notas.Length; i++)
{
Console.Write($"Nota do aluno {i + 1}: ");
notas[i] = double.Parse(Console.ReadLine()!);
// some a nota em soma
}
Console.WriteLine($"Notas: {string.Join(" | ", notas)}");
Console.WriteLine($"Média da turma: {soma / notas.Length:N1}");
const int Quantidade = 5;
double[] notas = new double[Quantidade];
double soma = 0;
Console.WriteLine("=== Boletim da Turma ===");
for (int i = 0; i < notas.Length; i++)
{
Console.Write($"Nota do aluno {i + 1}: ");
notas[i] = double.Parse(Console.ReadLine()!);
soma += notas[i];
}
Console.WriteLine($"Notas: {string.Join(" | ", notas)}");
Console.WriteLine($"Média da turma: {soma / notas.Length:N1}");
Versão 2 · Nome e nota juntos: um array de records
Notas sem nome não servem para um boletim. Daria para ter dois arrays, um de nomes e um de notas, e cuidar para a posição 2 de um combinar com a posição 2 do outro. Mais simples: um tipo novo, Aluno, que guarda o nome e a nota de uma pessoa. Em C#, a forma mais curta de criar um tipo assim é um record.
const int Quantidade = 5;
Aluno[] turma = new Aluno[Quantidade];
double soma = 0;
Console.WriteLine("=== Boletim da Turma ===");
for (int i = 0; i < turma.Length; i++)
{
Console.Write($"Nome do aluno {i + 1}: ");
string nome = Console.ReadLine()!;
Console.Write($"Nota de {nome}: ");
double nota = double.Parse(Console.ReadLine()!);
turma[i] = new Aluno(nome, nota);
soma += nota;
}
Console.WriteLine();
foreach (Aluno aluno in turma)
{
Console.WriteLine($"{aluno.Nome}: {aluno.Nota:N1}");
}
Console.WriteLine($"Média da turma: {soma / turma.Length:N1}");
record Aluno(string Nome, double Nota);
- record Aluno(string Nome, double Nota);
- Uma linha cria um tipo com duas propriedades,
NomeeNota, e um construtor que recebe as duas. Fica no fim do arquivo: num arquivo com instruções de nível superior, os tipos vêm depois delas (Program.cs e o Main). - Aluno[] turma = new Aluno[Quantidade];
- Um array de 5 lugares para alunos. Ainda vazios: cada posição começa com
null. - Console.Write($"Nota de {nome}: ");
- A pergunta usa o nome que acabou de ser lido: a Ana sabe de quem é a nota.
- turma[i] = new Aluno(nome, nota);
- Cria o aluno e o guarda na posição
i. - foreach (Aluno aluno in turma)
- "Para cada aluno da turma": o foreach passa por todos, do primeiro ao último, sem índice e sem risco de passar do fim.
- {aluno.Nota:N1}
- Sempre com uma casa:
9aparece como9,0.
$ dotnet run boletim.cs === Boletim da Turma === Nome do aluno 1: Bruno Nota de Bruno: 7,5 Nome do aluno 2: Carla Nota de Carla: 9 Nome do aluno 3: Diego Nota de Diego: 5,5 Nome do aluno 4: Elisa Nota de Elisa: 8 Nome do aluno 5: Gabriel Nota de Gabriel: 3,5 Bruno: 7,5 Carla: 9,0 Diego: 5,5 Elisa: 8,0 Gabriel: 3,5 Média da turma: 6,7
Como o nome é lido numa linha só dele, nomes com espaço, como Ana Paula, funcionam sem nenhum cuidado a mais.
CS8852
A Ana errou a nota do Bruno e tentou trocar direto:
Aluno bruno = new Aluno("Bruno", 7.5);
bruno.Nota = 8;
Console.WriteLine(bruno);
record Aluno(string Nome, double Nota);
$ dotnet run nota.cs /home/ana/cs/boletim/nota.cs(2,1): error CS8852: A propriedade somente de inicialização ou o indexador 'Aluno.Nota' 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'. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: as propriedades de um record criado assim são init: só recebem valor na criação do objeto (veja readonly e init). Depois, o aluno não muda. Isso é de propósito: um dado que não muda não pode ser estragado por engano em outra parte do programa.
Como consertar: crie um aluno novo, igual ao antigo mas com a nota trocada, com with:
Aluno bruno = new Aluno("Bruno", 7.5);
Aluno corrigido = bruno with { Nota = 8 };
Console.WriteLine(bruno);
Console.WriteLine(corrigido);
record Aluno(string Nome, double Nota);
$ dotnet run nota.cs
Aluno { Nome = Bruno, Nota = 7,5 }
Aluno { Nome = Bruno, Nota = 8 }
De quebra, o record sabe se mostrar: o Console.WriteLine(bruno) escreve o nome do tipo e as propriedades. No boletim, basta guardar o aluno corrigido no lugar do antigo: turma[0] = turma[0] with { Nota = 8 };.
Missão (versão 2): o programa lê os cinco alunos, mas só mostra a média. Antes dela, escreva um foreach que mostra cada aluno no formato Bruno: 7,5, usando aluno.Nome e aluno.Nota com :N1.
const int Quantidade = 5;
Aluno[] turma = new Aluno[Quantidade];
double soma = 0;
Console.WriteLine("=== Boletim da Turma ===");
for (int i = 0; i < turma.Length; i++)
{
Console.Write($"Nome do aluno {i + 1}: ");
string nome = Console.ReadLine()!;
Console.Write($"Nota de {nome}: ");
double nota = double.Parse(Console.ReadLine()!);
turma[i] = new Aluno(nome, nota);
soma += nota;
}
Console.WriteLine();
// mostre cada aluno no formato "Bruno: 7,5"
Console.WriteLine($"Média da turma: {soma / turma.Length:N1}");
record Aluno(string Nome, double Nota);
const int Quantidade = 5;
Aluno[] turma = new Aluno[Quantidade];
double soma = 0;
Console.WriteLine("=== Boletim da Turma ===");
for (int i = 0; i < turma.Length; i++)
{
Console.Write($"Nome do aluno {i + 1}: ");
string nome = Console.ReadLine()!;
Console.Write($"Nota de {nome}: ");
double nota = double.Parse(Console.ReadLine()!);
turma[i] = new Aluno(nome, nota);
soma += nota;
}
Console.WriteLine();
foreach (Aluno aluno in turma)
{
Console.WriteLine($"{aluno.Nome}: {aluno.Nota:N1}");
}
Console.WriteLine($"Média da turma: {soma / turma.Length:N1}");
record Aluno(string Nome, double Nota);
Versão 3 · Maior, menor e aprovados
Agora as estatísticas. Para achar a maior nota, o jeito clássico é: "o maior até agora é o primeiro aluno; para cada um dos outros, se a nota dele for maior, ele passa a ser o maior". Guardamos o aluno inteiro, e não só a nota, porque assim sabemos também o nome. A menor nota é igual, com <. E os aprovados são um contador que sobe quando a nota é pelo menos 6.
const int Quantidade = 5;
const double MediaParaAprovar = 6.0;
Aluno[] turma = new Aluno[Quantidade];
double soma = 0;
Console.WriteLine("=== Boletim da Turma ===");
for (int i = 0; i < turma.Length; i++)
{
Console.Write($"Nome do aluno {i + 1}: ");
string nome = Console.ReadLine()!;
Console.Write($"Nota de {nome}: ");
double nota = double.Parse(Console.ReadLine()!);
turma[i] = new Aluno(nome, nota);
soma += nota;
}
Aluno maior = turma[0];
Aluno menor = turma[0];
int aprovados = 0;
foreach (Aluno aluno in turma)
{
if (aluno.Nota > maior.Nota)
{
maior = aluno;
}
if (aluno.Nota < menor.Nota)
{
menor = aluno;
}
if (aluno.Nota >= MediaParaAprovar)
{
aprovados++;
}
}
Console.WriteLine();
Console.WriteLine($"Média da turma: {soma / turma.Length:N1}");
Console.WriteLine($"Maior nota: {maior.Nota:N1} ({maior.Nome})");
Console.WriteLine($"Menor nota: {menor.Nota:N1} ({menor.Nome})");
Console.WriteLine($"Aprovados: {aprovados} de {turma.Length}");
record Aluno(string Nome, double Nota);
- const double MediaParaAprovar = 6.0;
- A nota mínima para passar, com nome. Ninguém precisa adivinhar o que é aquele
6.0no meio do código. - Aluno maior = turma[0];
- O maior "até agora" começa sendo o primeiro aluno.
- if (aluno.Nota > maior.Nota) maior = aluno;
- Compara com o maior até agora; se ganhou, passa a ser ele.
- if (aluno.Nota >= MediaParaAprovar) aprovados++;
- Conta quem passou.
>=: quem tirou exatamente 6 também passa. - maior.Nome
- Com o aluno guardado, temos todos os dados dele.
$ dotnet run boletim.cs === Boletim da Turma === Nome do aluno 1: Bruno Nota de Bruno: 7,5 Nome do aluno 2: Carla Nota de Carla: 9 Nome do aluno 3: Diego Nota de Diego: 5,5 Nome do aluno 4: Elisa Nota de Elisa: 8 Nome do aluno 5: Gabriel Nota de Gabriel: 3,5 Média da turma: 6,7 Maior nota: 9,0 (Carla) Menor nota: 3,5 (Gabriel) Aprovados: 3 de 5
Com >, o primeiro aluno com a maior nota fica com o título; com >=, ficaria o último. Nenhum dos dois está errado, mas é bom saber qual você escolheu.
Quando você chegar ao LINQ, as mesmas contas cabem em uma linha cada: turma.Average(a => a.Nota), turma.MaxBy(a => a.Nota) e turma.Count(a => a.Nota >= MediaParaAprovar) (veja Agregar). Fazer o laço à mão primeiro ajuda a entender o que o LINQ faz por dentro.
Missão (versão 3): o programa já acha a maior nota, mas a menor sai sempre a do Bruno e os aprovados dão 0. No foreach, atualize menor (como foi feito com maior, mas com <) e conte os aprovados (aluno.Nota >= MediaParaAprovar).
const int Quantidade = 5;
const double MediaParaAprovar = 6.0;
Aluno[] turma = new Aluno[Quantidade];
double soma = 0;
Console.WriteLine("=== Boletim da Turma ===");
for (int i = 0; i < turma.Length; i++)
{
Console.Write($"Nome do aluno {i + 1}: ");
string nome = Console.ReadLine()!;
Console.Write($"Nota de {nome}: ");
double nota = double.Parse(Console.ReadLine()!);
turma[i] = new Aluno(nome, nota);
soma += nota;
}
Aluno maior = turma[0];
Aluno menor = turma[0];
int aprovados = 0;
foreach (Aluno aluno in turma)
{
if (aluno.Nota > maior.Nota)
{
maior = aluno;
}
// atualize "menor" e conte os aprovados
}
Console.WriteLine();
Console.WriteLine($"Média da turma: {soma / turma.Length:N1}");
Console.WriteLine($"Maior nota: {maior.Nota:N1} ({maior.Nome})");
Console.WriteLine($"Menor nota: {menor.Nota:N1} ({menor.Nome})");
Console.WriteLine($"Aprovados: {aprovados} de {turma.Length}");
record Aluno(string Nome, double Nota);
const int Quantidade = 5;
const double MediaParaAprovar = 6.0;
Aluno[] turma = new Aluno[Quantidade];
double soma = 0;
Console.WriteLine("=== Boletim da Turma ===");
for (int i = 0; i < turma.Length; i++)
{
Console.Write($"Nome do aluno {i + 1}: ");
string nome = Console.ReadLine()!;
Console.Write($"Nota de {nome}: ");
double nota = double.Parse(Console.ReadLine()!);
turma[i] = new Aluno(nome, nota);
soma += nota;
}
Aluno maior = turma[0];
Aluno menor = turma[0];
int aprovados = 0;
foreach (Aluno aluno in turma)
{
if (aluno.Nota > maior.Nota)
{
maior = aluno;
}
if (aluno.Nota < menor.Nota)
{
menor = aluno;
}
if (aluno.Nota >= MediaParaAprovar)
{
aprovados++;
}
}
Console.WriteLine();
Console.WriteLine($"Média da turma: {soma / turma.Length:N1}");
Console.WriteLine($"Maior nota: {maior.Nota:N1} ({maior.Nome})");
Console.WriteLine($"Menor nota: {menor.Nota:N1} ({menor.Nome})");
Console.WriteLine($"Aprovados: {aprovados} de {turma.Length}");
record Aluno(string Nome, double Nota);
Versão 4 · A tabela alinhada
A versão final tem quatro novidades:
- A quantidade de alunos é perguntada no começo (de 1 a 30), e o array é criado com o tamanho exato.
- Um método
LerNotapergunta de novo enquanto a nota não for um número de 0 a 10. - Um método
Situacao, feito com uma expressão switch, devolve"Aprovado","Recuperação"(de 4 a 5,9) ou"Reprovado". - Uma tabela, com as colunas alinhadas pela largura dentro da interpolação.
const int MaxAlunos = 30;
const double MediaParaAprovar = 6.0;
Console.WriteLine("=== Boletim da Turma ===");
Console.Write($"Quantos alunos (1 a {MaxAlunos})? ");
if (!int.TryParse(Console.ReadLine(), out int quantidade) || quantidade < 1 || quantidade > MaxAlunos)
{
Console.WriteLine("Quantidade inválida.");
return;
}
Aluno[] turma = new Aluno[quantidade];
for (int i = 0; i < turma.Length; i++)
{
Console.Write($"Nome do aluno {i + 1}: ");
string nome = (Console.ReadLine() ?? "").Trim();
double nota = LerNota(nome);
turma[i] = new Aluno(nome, nota);
}
double soma = 0;
Aluno maior = turma[0];
Aluno menor = turma[0];
int aprovados = 0;
Console.WriteLine();
Console.WriteLine($"{"Aluno",-10} {"Nota",5} Situação");
Console.WriteLine("---------- ----- -----------");
foreach (Aluno aluno in turma)
{
Console.WriteLine($"{aluno.Nome,-10} {aluno.Nota,5:N1} {Situacao(aluno.Nota)}");
soma += aluno.Nota;
if (aluno.Nota > maior.Nota)
{
maior = aluno;
}
if (aluno.Nota < menor.Nota)
{
menor = aluno;
}
if (aluno.Nota >= MediaParaAprovar)
{
aprovados++;
}
}
Console.WriteLine("---------- ----- -----------");
Console.WriteLine($"{"Média",-10} {soma / turma.Length,5:N1}");
Console.WriteLine();
Console.WriteLine($"Maior nota: {maior.Nota:N1} ({maior.Nome})");
Console.WriteLine($"Menor nota: {menor.Nota:N1} ({menor.Nome})");
Console.WriteLine($"Aprovados: {aprovados} de {turma.Length}");
static double LerNota(string nome)
{
while (true)
{
Console.Write($"Nota de {nome} (0 a 10): ");
string? texto = Console.ReadLine();
if (texto is null)
{
Console.WriteLine();
Console.WriteLine("A entrada acabou antes do fim.");
Environment.Exit(1);
}
if (double.TryParse(texto, out double nota) && nota >= 0 && nota <= 10)
{
return nota;
}
Console.WriteLine("Nota inválida. Digite um número de 0 a 10.");
}
}
static string Situacao(double nota) => nota switch
{
>= MediaParaAprovar => "Aprovado",
>= 4.0 => "Recuperação",
_ => "Reprovado"
};
record Aluno(string Nome, double Nota);
| Na interpolação | O que faz | Exemplo |
|---|---|---|
{aluno.Nome,-10} | Texto numa coluna de 10 posições, encostado à esquerda (o -); o resto é preenchido com espaços. | [Bruno ] |
{aluno.Nome,10} | O mesmo, encostado à direita. | [ Bruno] |
{aluno.Nota,5:N1} | Primeiro a largura, depois o formato: 1 casa decimal numa coluna de 5, à direita. As vírgulas ficam uma embaixo da outra. | [ 7,5] |
{"Nota",5} | O título com a mesma largura dos números. | [ Nota] |
- if (!int.TryParse(…) || quantidade < 1 || quantidade > MaxAlunos)
- Recusa a quantidade se não for número, se for menor que 1 ou maior que 30.
- Aluno[] turma = new Aluno[quantidade];
- O array com o tamanho exato da turma. Veja no cenário abaixo por que isso importa.
- (Console.ReadLine() ?? "").Trim()
- Tira os espaços que a Ana digitou sem querer antes ou depois do nome, para não estragar o alinhamento.
- static double LerNota(string nome)
- Pergunta até vir uma nota válida e a devolve.
&&exige as três coisas: ser número, ser pelo menos 0 e no máximo 10. - Environment.Exit(1);
- Se a entrada acabou (
null), não há como terminar o boletim: o programa encerra com o código de saída 1. Sem isso, o laço perguntaria a nota para sempre. - nota switch { >= MediaParaAprovar => "Aprovado", … }
- Uma expressão switch com padrões relacionais: testa de cima para baixo e devolve o texto do primeiro que combina. O
_é "qualquer outro valor". O=>logo depois deSituacao(double nota)faz do método uma linha só (um expression-bodied member).
$ dotnet run boletim.cs === Boletim da Turma === Quantos alunos (1 a 30)? 3 Nome do aluno 1: Bruno Nota de Bruno (0 a 10): 7,5 Nome do aluno 2: Fábio Nota de Fábio (0 a 10): 6 Nome do aluno 3: Ana Paula Nota de Ana Paula (0 a 10): 11 Nota inválida. Digite um número de 0 a 10. Nota de Ana Paula (0 a 10): 8 Aluno Nota Situação ---------- ----- ----------- Bruno 7,5 Aprovado Fábio 6,0 Aprovado Ana Paula 8,0 Aprovado ---------- ----- ----------- Média 7,2 Maior nota: 8,0 (Ana Paula) Menor nota: 6,0 (Fábio) Aprovados: 3 de 3
O 11 foi recusado, e os nomes com acento (Fábio, Média) ficaram alinhados: no C#, a largura conta letras, não bytes. (Em C, o printf conta bytes, e cada letra acentuada desalinha a coluna.)
NullReferenceException
Na primeira tentativa, a Ana criou o array sempre com 30 lugares (new Aluno[MaxAlunos]) e leu só os quantidade primeiros:
$ dotnet run boletim.cs === Boletim da Turma === Quantos alunos (1 a 30)? 3 Nome do aluno 1: Bruno Nota de Bruno (0 a 10): 7,5 Nome do aluno 2: Carla Nota de Carla (0 a 10): 9 Nome do aluno 3: Diego Nota de Diego (0 a 10): 5,5 Aluno Nota Situação ---------- ----- ----------- Bruno 7,5 Aprovado Carla 9,0 Aprovado Diego 5,5 Recuperação Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Program.<Main>$(String[] args) in /home/ana/cs/boletim/boletim.cs:line 31
O que aconteceu: os três alunos saíram, e o programa caiu no quarto. O foreach passa pelas 30 posições, e da quarta em diante não há aluno: a posição guarda null. Na linha 31, aluno.Nome tenta ler o nome de "ninguém". O compilador não avisou, porque um new Aluno[30] parece um array de alunos, mas nasce cheio de null (veja Referências Anuláveis).
Como consertar: crie o array do tamanho certo, new Aluno[quantidade], como na versão final. Quando o tamanho não é conhecido de antemão, use uma List<Aluno>, que cresce a cada Add (é o desafio 1).
Missão (versão 4): a tabela sai torta: Bruno 7,5 Aprovado. Na linha de cada aluno, use as larguras: {aluno.Nome,-10} e {aluno.Nota,5:N1}, com dois espaços antes da situação, como no título. A entrada da missão é a turma da Ana.
const int MaxAlunos = 30;
const double MediaParaAprovar = 6.0;
Console.WriteLine("=== Boletim da Turma ===");
Console.Write($"Quantos alunos (1 a {MaxAlunos})? ");
if (!int.TryParse(Console.ReadLine(), out int quantidade) || quantidade < 1 || quantidade > MaxAlunos)
{
Console.WriteLine("Quantidade inválida.");
return;
}
Aluno[] turma = new Aluno[quantidade];
for (int i = 0; i < turma.Length; i++)
{
Console.Write($"Nome do aluno {i + 1}: ");
string nome = (Console.ReadLine() ?? "").Trim();
double nota = LerNota(nome);
turma[i] = new Aluno(nome, nota);
}
double soma = 0;
Aluno maior = turma[0];
Aluno menor = turma[0];
int aprovados = 0;
Console.WriteLine();
Console.WriteLine($"{"Aluno",-10} {"Nota",5} Situação");
Console.WriteLine("---------- ----- -----------");
foreach (Aluno aluno in turma)
{
Console.WriteLine($"{aluno.Nome} {aluno.Nota:N1} {Situacao(aluno.Nota)}");
soma += aluno.Nota;
if (aluno.Nota > maior.Nota)
{
maior = aluno;
}
if (aluno.Nota < menor.Nota)
{
menor = aluno;
}
if (aluno.Nota >= MediaParaAprovar)
{
aprovados++;
}
}
Console.WriteLine("---------- ----- -----------");
Console.WriteLine($"{"Média",-10} {soma / turma.Length,5:N1}");
Console.WriteLine();
Console.WriteLine($"Maior nota: {maior.Nota:N1} ({maior.Nome})");
Console.WriteLine($"Menor nota: {menor.Nota:N1} ({menor.Nome})");
Console.WriteLine($"Aprovados: {aprovados} de {turma.Length}");
static double LerNota(string nome)
{
while (true)
{
Console.Write($"Nota de {nome} (0 a 10): ");
string? texto = Console.ReadLine();
if (texto is null)
{
Console.WriteLine();
Console.WriteLine("A entrada acabou antes do fim.");
Environment.Exit(1);
}
if (double.TryParse(texto, out double nota) && nota >= 0 && nota <= 10)
{
return nota;
}
Console.WriteLine("Nota inválida. Digite um número de 0 a 10.");
}
}
static string Situacao(double nota) => nota switch
{
>= MediaParaAprovar => "Aprovado",
>= 4.0 => "Recuperação",
_ => "Reprovado"
};
record Aluno(string Nome, double Nota);
const int MaxAlunos = 30;
const double MediaParaAprovar = 6.0;
Console.WriteLine("=== Boletim da Turma ===");
Console.Write($"Quantos alunos (1 a {MaxAlunos})? ");
if (!int.TryParse(Console.ReadLine(), out int quantidade) || quantidade < 1 || quantidade > MaxAlunos)
{
Console.WriteLine("Quantidade inválida.");
return;
}
Aluno[] turma = new Aluno[quantidade];
for (int i = 0; i < turma.Length; i++)
{
Console.Write($"Nome do aluno {i + 1}: ");
string nome = (Console.ReadLine() ?? "").Trim();
double nota = LerNota(nome);
turma[i] = new Aluno(nome, nota);
}
double soma = 0;
Aluno maior = turma[0];
Aluno menor = turma[0];
int aprovados = 0;
Console.WriteLine();
Console.WriteLine($"{"Aluno",-10} {"Nota",5} Situação");
Console.WriteLine("---------- ----- -----------");
foreach (Aluno aluno in turma)
{
Console.WriteLine($"{aluno.Nome,-10} {aluno.Nota,5:N1} {Situacao(aluno.Nota)}");
soma += aluno.Nota;
if (aluno.Nota > maior.Nota)
{
maior = aluno;
}
if (aluno.Nota < menor.Nota)
{
menor = aluno;
}
if (aluno.Nota >= MediaParaAprovar)
{
aprovados++;
}
}
Console.WriteLine("---------- ----- -----------");
Console.WriteLine($"{"Média",-10} {soma / turma.Length,5:N1}");
Console.WriteLine();
Console.WriteLine($"Maior nota: {maior.Nota:N1} ({maior.Nome})");
Console.WriteLine($"Menor nota: {menor.Nota:N1} ({menor.Nome})");
Console.WriteLine($"Aprovados: {aprovados} de {turma.Length}");
static double LerNota(string nome)
{
while (true)
{
Console.Write($"Nota de {nome} (0 a 10): ");
string? texto = Console.ReadLine();
if (texto is null)
{
Console.WriteLine();
Console.WriteLine("A entrada acabou antes do fim.");
Environment.Exit(1);
}
if (double.TryParse(texto, out double nota) && nota >= 0 && nota <= 10)
{
return nota;
}
Console.WriteLine("Nota inválida. Digite um número de 0 a 10.");
}
}
static string Situacao(double nota) => nota switch
{
>= MediaParaAprovar => "Aprovado",
>= 4.0 => "Recuperação",
_ => "Reprovado"
};
record Aluno(string Nome, double Nota);
O programa completo
A versão final é a versão 4, acima. O que o projeto juntou: arrays, for e foreach, um record, constantes, TryParse, dois métodos, uma expressão switch e o alinhamento da interpolação.
Rodando com a turma num arquivo
Para não digitar a turma toda vez, a Ana guardou as respostas em turma.txt e mandou o arquivo para o programa com <. Como ninguém digitou, as respostas não aparecem depois das perguntas:
$ cat turma.txt 5 Bruno 7,5 Carla 9 Diego 5,5 Elisa 8 Gabriel 3,5 $ dotnet run boletim.cs < turma.txt === Boletim da Turma === Quantos alunos (1 a 30)? Nome do aluno 1: Nota de Bruno (0 a 10): Nome do aluno 2: Nota de Carla (0 a 10): Nome do aluno 3: Nota de Diego (0 a 10): Nome do aluno 4: Nota de Elisa (0 a 10): Nome do aluno 5: Nota de Gabriel (0 a 10): Aluno Nota Situação ---------- ----- ----------- Bruno 7,5 Aprovado Carla 9,0 Aprovado Diego 5,5 Recuperação Elisa 8,0 Aprovado Gabriel 3,5 Reprovado ---------- ----- ----------- Média 6,7 Maior nota: 9,0 (Carla) Menor nota: 3,5 (Gabriel) Aprovados: 3 de 5
Desafios extras
- Turma de qualquer tamanho. Troque o array por uma List<Aluno> e leia alunos até a Ana deixar o nome em branco. Não precisa mais perguntar a quantidade.
- Duas notas por aluno. Mude o record para
Aluno(string Nome, double Prova, double Trabalho)e acrescente uma propriedadeMediacalculada (veja Records e Propriedades). - Em ordem de nota. Mostre a tabela da maior para a menor nota com
Array.Sortou com o LINQOrderByDescending(veja A Classe Array e Ordenar). - Com LINQ. Troque o laço da maior, da menor e dos aprovados por
MaxBy,MinByeCount(veja Agregar). - Nomes compridos. Um nome com mais de 10 letras empurra a coluna. Corte com
nome[..10](veja Índices e Intervalos) ou calcule a largura pelo maior nome.
Exercícios
1. Um array criado com new double[5] tem quais posições válidas?
2. O que acontece em C# quando o programa acessa notas[5] num array de 5 posições?
3. O que $"[{"Ana",-6}]" mostra?
4. Por que bruno.Nota = 8; dá o erro CS8852?
5. Complete a interpolação que mostra a nota com uma casa decimal numa coluna de 5 posições:
6. Complete o "qualquer outro valor" da expressão switch:
Resumo
- Um array guarda muitos valores do mesmo tipo; as posições vão de
0aLength - 1, e o laço éi < array.Length. - Passar do fim lança
IndexOutOfRangeExceptioncom a linha exata; oforeachnunca passa do fim. - Um
recordjunta os dados de uma coisa numa linha; as propriedades não mudam depois da criação (withcria uma cópia alterada). - Um array de objetos nasce cheio de
null: crie-o do tamanho certo ou useList<T>. - Maior e menor: comece pelo primeiro e troque quando achar um melhor; guarde o objeto, não só o número.
{texto,-10}alinha à esquerda,{numero,5:N1}alinha à direita com formato; no C#, a largura conta letras, e os acentos não desalinham.
Próximo projeto: Agenda de Contatos (JSON).