C#: Exercício: LINQ e arquivos

1. Analisador de notas dos alunos

Serialize os dados dos alunos em um arquivo JSON; em seguida, deserialize-os e utilize o LINQ para realizar uma análise estatística multidimensional: nota média, os três melhores, número de aprovações e agrupamento por série.

(1) Requisitos

▶ Exemplo

CSHARP 📖 Somente leitura
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Collections.Generic;

public class Student
{
    public string Name { get; set; }
    public int Score { get; set; }
}

class Program
{
    static void Main()
    {
        var students = new List<Student>
        {
            new Student { Name = "Zhang San", Score = 92 },
            new Student { Name = "Li Si", Score = 45 },
            new Student { Name = "Wang Wu", Score = 78 },
            new Student { Name = "Zhao Liu", Score = 88 },
            new Student { Name = "Qian Qi", Score = 55 },
            new Student { Name = "Sun Ba", Score = 95 },
            new Student { Name = "Zhou Jiu", Score = 33 },
            new Student { Name = "Wu Shi", Score = 71 }
        };

        string tempDir = Path.GetTempPath();
        string filePath = Path.Combine(tempDir, "students.json");

        var options = new JsonSerializerOptions { WriteIndented = true };
        string json = JsonSerializer.Serialize(students, options);
        File.WriteAllText(filePath, json);
        Console.WriteLine("Written to: " + filePath);

        string readJson = File.ReadAllText(filePath);
        var loaded = JsonSerializer.Deserialize<List<Student>>(readJson);

        double average = loaded.Average(s => s.Score);
        Console.WriteLine($"Average score: {average:F1}");

        var top3 = loaded.OrderByDescending(s => s.Score).Take(3);
        Console.WriteLine("Top three:");
        foreach (var s in top3)
        {
            Console.WriteLine($"  {s.Name} - {s.Score}");
        }

        int passCount = loaded.Count(s => s.Score >= 60);
        Console.WriteLine($"Pass count: {passCount}");

        var grouped = loaded.GroupBy(s => s.Score >= 90 ? "Excellent"
            : s.Score >= 60 ? "Pass" : "Fail");
        Console.WriteLine("Grade groups:");
        foreach (var group in grouped)
        {
            Console.WriteLine($"  {group.Key}: {string.Join(", ", group.Select(s => s.Name))}");
        }

        File.Delete(filePath);
    }
}
53 linhas de lógica (limite de 40, somente leitura)
TEXT 📖 Somente leitura
Written to: /tmp/students.json
Average score: 69.6
Top three:
  Sun Ba - 95
  Zhang San - 92
  Zhao Liu - 88
Pass count: 5
Grade groups:
  Excellent: Zhang San, Sun Ba
  Fail: Li Si, Qian Qi, Zhou Jiu
  Pass: Wang Wu, Zhao Liu, Wu Shi

2. Analisador Simples de Logs

Leia um arquivo de log de texto, analise cada linha e use o LINQ para contar erros e avisos, identificar o erro mais frequente e filtrar por intervalo de datas.

(1) Requisitos

▶ Exemplo

CSHARP 📖 Somente leitura
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        string tempDir = Path.GetTempPath();
        string logPath = Path.Combine(tempDir, "app.log");

        var lines = new List```<string>```
        {
            "[2025-01-10] [INFO] System started",
            "[2025-01-10] [WARN] Memory usage 80%",
            "[2025-01-11] [ERROR] Database connection failed",
            "[2025-01-11] [ERROR] Disk full",
            "[2025-01-12] [INFO] User login",
            "[2025-01-12] [ERROR] Database connection failed",
            "[2025-01-13] [WARN] CPU usage 90%",
            "[2025-01-13] [ERROR] Network timeout",
            "[2025-01-14] [INFO] Scheduled task completed",
            "[2025-01-14] [ERROR] Database connection failed"
        };
        File.WriteAllLines(logPath, lines);

        var logEntries = File.ReadAllLines(logPath)
            .Select(line =>
            {
                var parts = line.Split(']');
                return new
                {
                    Date = parts[0].TrimStart('[').Trim(),
                    Level = parts[1].TrimStart('[').Trim(),
                    Message = parts[2].Trim()
                };
            })
            .ToList();

        var levelCounts = logEntries
            .GroupBy(e => e.Level)
            .Select(g => new { Level = g.Key, Count = g.Count() })
            .OrderByDescending(x => x.Count);
        Console.WriteLine("Level counts:");
        foreach (var item in levelCounts)
        {
            Console.WriteLine($"  {item.Level}: {item.Count}");
        }

        var topError = logEntries
            .Where(e => e.Level == "ERROR")
            .GroupBy(e => e.Message)
            .OrderByDescending(g => g.Count())
            .First();
        Console.WriteLine($"Most frequent error: {topError.Key} ({topError.Count()} times)");

        var filtered = logEntries
            .Where(e => string.Compare(e.Date, "2025-01-11") >= 0
                     && string.Compare(e.Date, "2025-01-13") <= 0)
            .ToList();
        Console.WriteLine("Logs from Jan 11-13:");
        foreach (var entry in filtered)
        {
            Console.WriteLine($"  [{entry.Date}] [{entry.Level}] {entry.Message}");
        }

        File.Delete(logPath);
    }
}
63 linhas de lógica (limite de 40, somente leitura)
TEXT 📖 Somente leitura
Level counts:
  ERROR: 5
  INFO: 3
  WARN: 2
Most frequent error: Database connection failed (3 times)
Logs from Jan 11-13:
  [2025-01-11] [ERROR] Database connection failed
  [2025-01-11] [ERROR] Disk full
  [2025-01-12] [INFO] User login
  [2025-01-12] [ERROR] Database connection failed
  [2025-01-13] [WARN] CPU usage 90%
  [2025-01-13] [ERROR] Network timeout

3. Ferramenta para renomear arquivos em lote

Listar os arquivos de um diretório, filtrar por extensão usando LINQ e, em seguida, adicionar em lote um prefixo ou sufixo.

(1) Requisitos

▶ Exemplo

CSHARP 📖 Somente leitura
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        string workDir = Path.Combine(Path.GetTempPath(), "rename_demo");
        Directory.CreateDirectory(workDir);

        var demoFiles = new[] { "photo1.jpg", "photo2.jpg", "doc.txt", "notes.txt", "data.csv" };
        foreach (var f in demoFiles)
        {
            File.WriteAllText(Path.Combine(workDir, f), "demo");
        }
        Console.WriteLine("Original files:");
        foreach (var f in Directory.GetFiles(workDir))
        {
            Console.WriteLine("  " + Path.GetFileName(f));
        }

        string targetExt = ".txt";
        string prefix = "backup_";

        var filesToRename = Directory.GetFiles(workDir)
            .Where(f => Path.GetExtension(f).Equals(targetExt, StringComparison.OrdinalIgnoreCase))
            .ToList();

        Console.WriteLine($"\nFiltering extension {targetExt}:");
        foreach (var f in filesToRename)
        {
            string dir = Path.GetDirectoryName(f);
            string nameNoExt = Path.GetFileNameWithoutExtension(f);
            string newName = prefix + nameNoExt + targetExt;
            string newPath = Path.Combine(dir, newName);
            Console.WriteLine($"  {Path.GetFileName(f)} -> {newName}");
            File.Move(f, newPath);
        }

        Console.WriteLine("\nAfter renaming:");
        foreach (var f in Directory.GetFiles(workDir))
        {
            Console.WriteLine("  " + Path.GetFileName(f));
        }

        Directory.Delete(workDir, true);
    }
}
43 linhas de lógica (limite de 40, somente leitura)
TEXT 📖 Somente leitura
Original files:
  photo1.jpg
  photo2.jpg
  doc.txt
  notes.txt
  data.csv

Filtering extension .txt:
  doc.txt -> backup_doc.txt
  notes.txt -> backup_notes.txt

After renaming:
  photo1.jpg
  photo2.jpg
  backup_doc.txt
  backup_notes.txt
  data.csv

❓ Perguntas Frequentes

P: E se os nomes das propriedades não corresponderem durante a desserialização de JSON? R: Use o atributo [JsonPropertyName] para mapear os nomes ou personalize PropertyNamingPolicy em JsonSerializerOptions.

P: A ordem dos resultados do GroupBy é garantida? R: Não, o GroupBy não garante a ordem dos grupos. Use o OrderBy para ordenar antes de iterar.

P: O que acontece se o arquivo de destino já existir ao usar File.Move para renomear? R: É lançada uma IOException. Você deve verificar primeiro se o caminho de destino existe ou usar File.Replace.

P: O ReadAllLines é adequado para ler arquivos de log grandes? R: Não, o ReadAllLines carrega todo o conteúdo na memória de uma só vez. Para arquivos grandes, use o File.ReadLines para uma leitura gradual, linha por linha.

📖 Resumo

📝 Exercícios

  1. Ampliar o analisador de notas dos alunos para contabilizar os alunos por faixa de pontuação (0-59/60-79/80-89/90-100) e gerar um gráfico de barras (utilizando os caracteres *)
  2. Adicione um recurso ao analisador de logs: conte as entradas de ERROR por dia e exiba o intervalo de datas com o maior número de ocorrências consecutivas de ERROR
  3. Refatorar a ferramenta de renomeação de arquivos para aceitar argumentos de linha de comando para caminho do diretório, filtro de extensão e prefixo, e adicionar um modo --dry-run que apenas mostre uma pré-visualização sem executar
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%