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ó).

Você vai praticar
  • 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 Aluno e 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
  1. As notas num array e a média.
  2. Nome e nota juntos: um array de records.
  3. Maior, menor e aprovados.
  4. A tabela alinhada (quantidade de alunos, LerNota e 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}");
LinhaO 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, Nome e Nota, 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: 9 aparece como 9,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.

Corrigir uma nota: erro 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.0 no 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
E se houver empate? E o LINQ?

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 LerNota pergunta 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çãoO que fazExemplo
{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 de Situacao(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.)

Um array de 30 para 3 alunos: 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

  1. 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.
  2. Duas notas por aluno. Mude o record para Aluno(string Nome, double Prova, double Trabalho) e acrescente uma propriedade Media calculada (veja Records e Propriedades).
  3. Em ordem de nota. Mostre a tabela da maior para a menor nota com Array.Sort ou com o LINQ OrderByDescending (veja A Classe Array e Ordenar).
  4. Com LINQ. Troque o laço da maior, da menor e dos aprovados por MaxBy, MinBy e Count (veja Agregar).
  5. 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:

$"{aluno.Nota,}"

6. Complete o "qualquer outro valor" da expressão switch:

=> "Reprovado"

Resumo

  • Um array guarda muitos valores do mesmo tipo; as posições vão de 0 a Length - 1, e o laço é i < array.Length.
  • Passar do fim lança IndexOutOfRangeException com a linha exata; o foreach nunca passa do fim.
  • Um record junta os dados de uma coisa numa linha; as propriedades não mudam depois da criação (with cria uma cópia alterada).
  • Um array de objetos nasce cheio de null: crie-o do tamanho certo ou use List<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).