C#: 演習:LINQ とファイル

1. 学生の成績分析ツール

学生データをJSONファイルにシリアライズし、それをデシリアライズして、LINQを用いて多次元統計分析(平均点、上位3名、合格者数、学年ごとのグループ分け)を行います。

(1) 要件

▶ サンプル

CSHARP 📖 参照専用
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 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
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. シンプルなログ解析ツール

テキスト形式のログファイルを読み込み、各行を解析し、LINQ を使用してエラーや警告の件数をカウントし、最も頻度の高いエラーを特定し、日付範囲でフィルタリングします。

(1) 要件

▶ サンプル

CSHARP 📖 参照専用
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 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
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. バッチファイル名変更ツール

ディレクトリ内のファイルを一覧表示し、LINQ を使用して拡張子でフィルタリングした後、接頭辞または接尾辞を一括で追加します。

(1) 要件

▶ サンプル

CSHARP 📖 参照専用
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 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
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

❓ よくある質問

Q JSONのデシリアライズ時にプロパティ名が一致しない場合はどうすればよいですか?
A [JsonPropertyName]属性を使用して名前をマッピングするか、JsonSerializerOptions内のPropertyNamingPolicyをカスタマイズしてください。
Q GroupBy の結果の順序は保証されていますか?
A いいえ、GroupBy ではグループの順序は保証されません。反復処理を行う前に、OrderBy を使用して並べ替えてください。
Q File.Move を使用してファイル名を変更する際、対象のファイルがすでに存在している場合はどうなりますか?
A IOException が発生します。まず対象のパスが存在するかどうかを確認するか、File.Replace を使用してください。
Q ReadAllLines は、大容量のログファイルを読み込むのに適していますか?
A いいえ、ReadAllLines はコンテンツ全体を一度にメモリに読み込みます。大容量のファイルの場合は、File.ReadLines を使用して、行単位で遅延読み込みを行ってください。

📖 まとめ

📝 練習問題

  1. 学生の成績分析機能を拡張し、得点範囲(0-59/60-79/80-89/90-100)ごとに学生数を集計し、棒グラフを出力するようにする(*文字を使用)。
  2. ログアナライザーに以下の機能を追加する:1日あたりの「ERROR」エントリ数をカウントし、「ERROR」が最も連続して発生した日付範囲を出力する
  3. ファイル名変更ツールをリファクタリングし、ディレクトリパス、拡張子フィルター、プレフィックスを指定するためのコマンドライン引数を受け入れられるようにするとともに、実行せずにプレビューのみを表示する--dry-runモードを追加する
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%