基礎から学ぶC#入門 90日コース | アプリを作りながら学ぶ - Day 79:例外処理と入力チェック

Web APP 90日で身につけるC#
スポンサーリンク
スポンサーリンク

Day 79 のゴールと全体像

Day 79 では、タスク管理アプリを 「ちょっとやそっとでは落ちない、頼れるプログラム」 に育てていきます。 テーマはずばり、

  • 例外処理
  • 入力チェック

です。

ここまでのコードでも、ある程度入力チェックはしてきましたが、 まだ「変な入力をされるとクラッシュする可能性」が残っています。

今日は、

不正な入力でも落ちない ユーザーに優しくエラーメッセージを返す

そんなプログラムにしていくための考え方と、具体的なコードを、 ステップバイステップでかみ砕いて解説していきます。

例外ってそもそも何?

「想定外の事態」が起きたときのサイン

C# では、プログラムの実行中に「想定外の事態」が起きると、 例外(Exception) が投げられます。

例えば、

  • 存在しないファイルを読み込もうとした
  • 数値に変換できない文字列を int.Parse した
  • ネットワークが切断されているのに通信しようとした

こういったときに、 「このまま続けると危ないよ!」というサインとして、 例外が発生します。

例外を何も処理しないと、 プログラムはその場でクラッシュして終了してしまいます。

例外処理の基本:try / catch

まずは形を覚える

例外処理の基本形は、

try
{
    // 例外が起きるかもしれない処理
}
catch (Exception ex)
{
    // 例外が起きたときの対応
}
C#

です。

try ブロックの中で例外が発生すると、 その瞬間に catch ブロックにジャンプし、 そこで「どうするか」を決めることができます。

入力チェックと例外処理の役割分担

入力チェックは「事前に防ぐ」

コンソールアプリでは、ユーザーが自由に文字を入力できます。 そのため、

  • 数字が欲しい場面で「abc」と入力される
  • 日付が欲しい場面で「2026-99-99」と入力される

といったことが普通に起こります。

こうした「おかしな入力」は、

  • 入力チェック(バリデーション)で事前に防ぐ
  • それでも防ぎきれないものは 例外処理で受け止める

という二段構えで考えると、プログラムがぐっと安定します。

まずは入力チェックを強化する

メニュー選択の入力チェック

Day 73〜78 で使ってきたメニューに、 もう少し丁寧な入力チェックを入れてみます。

static void Run()
{
    while (true)
    {
        Console.WriteLine("=== タスク管理アプリ ===");
        Console.WriteLine("1: タスク追加");
        Console.WriteLine("2: タスク一覧");
        Console.WriteLine("3: タスク編集");
        Console.WriteLine("4: 完了状態の変更");
        Console.WriteLine("5: タスク削除");
        Console.WriteLine("0: 終了");
        Console.Write("選択してください: ");

        string input = Console.ReadLine();

        // 入力が空の場合のチェック
        if (string.IsNullOrWhiteSpace(input))
        {
            Console.WriteLine("入力が空です。メニュー番号を入力してください。");
            Console.WriteLine();
            continue;
        }

        switch (input)
        {
            case "1":
                CreateTask();
                break;
            case "2":
                ShowTasks();
                break;
            case "3":
                EditTask();
                break;
            case "4":
                ToggleComplete();
                break;
            case "5":
                DeleteTask();
                break;
            case "0":
                Console.WriteLine("終了します。");
                return;
            default:
                Console.WriteLine("不正な入力です。メニューに表示されている番号を入力してください。");
                break;
        }

        Console.WriteLine();
    }
}
C#

ポイント

  • 空入力のチェック Enterだけ押されることもあるので、 string.IsNullOrWhiteSpace で「空 or 空白だけ」を検出しています。
  • メッセージを優しく 「不正な入力です」だけでなく、 「メニューに表示されている番号を入力してください」と 次に何をすればいいかを伝えると、ユーザーにとって親切です。

数値入力の安全な受け取り方

IDや優先度の入力で使うテンプレート

タスク編集や削除では、IDを数値として受け取る必要があります。 ここを丁寧に書き直してみましょう。

static int ReadInt(string label)
{
    while (true)
    {
        Console.Write($"{label}を入力してください: ");
        string text = Console.ReadLine();

        if (string.IsNullOrWhiteSpace(text))
        {
            Console.WriteLine("入力が空です。数値を入力してください。");
            continue;
        }

        if (int.TryParse(text, out int value))
        {
            return value;
        }

        Console.WriteLine("数値として認識できませんでした。もう一度入力してください。");
    }
}
C#

このメソッドを使うと、

int id = ReadInt("編集したいタスクのID");
int priority = ReadInt("優先度 (1: 高, 2: 中, 3: 低)");
C#

のように書けます。

重要ポイント

  • int.Parse は例外を投げますが、 int.TryParse例外を投げずに成功/失敗を返す メソッドです。
  • 失敗したときに「もう一度入力してもらう」ループにすることで、 プログラムが落ちることなく、ユーザーに再入力を促せます。

日付入力の安全な受け取り方

期限入力のテンプレート

期限(DueDate)の入力も、 日付の形式が違うと例外の原因になります。

これも、TryParse を使って安全に受け取りましょう。

static DateTime? ReadDateOrNull(string label)
{
    while (true)
    {
        Console.Write($"{label}を入力してください (yyyy-MM-dd、未入力なら期限なし): ");
        string text = Console.ReadLine();

        // 空なら期限なし(null)を返す
        if (string.IsNullOrWhiteSpace(text))
        {
            return null;
        }

        // 日付として解釈できるか試す
        if (DateTime.TryParse(text, out DateTime date))
        {
            return date;
        }

        Console.WriteLine("日付の形式が正しくありません。例: 2026-09-10 のように入力してください。");
    }
}
C#

これを使うと、

DateTime? dueDate = ReadDateOrNull("期限");
C#

と書けます。

例外処理を「最後の砦」として置く

メインループ全体を try / catch で包む

入力チェックをしっかり書いても、 それでも予期せぬ例外が起きることはあります。

そこで、アプリ全体を守る「最後の砦」として、 メインループを try / catch で包んでおくのは、とても良い習慣です。

static void Main()
{
    const string TaskFilePath = "tasks.json";

    try
    {
        // 起動時にタスクを読み込む
        repository.LoadFromFile(TaskFilePath);

        // メインループ
        Run();

        // 終了時にタスクを保存する
        repository.SaveToFile(TaskFilePath);
    }
    catch (Exception ex)
    {
        // 予期せぬ例外が発生した場合でも、ユーザーにメッセージを出して終了する
        Console.WriteLine("予期しないエラーが発生しました。アプリを終了します。");
        Console.WriteLine($"詳細: {ex.Message}");
    }
}
C#

ここで意識したいこと

  • 例外を「握りつぶす」のではなく、 ユーザーに「何かおかしなことが起きた」ことを伝えつつ、 プログラムを安全に終了させる、というスタンスが大事です。
  • 学習中は、ex.Message を表示しておくと、 「どんな例外が起きたのか」を知る手がかりになります。

JSON読み書きにも例外処理を足してみる

ファイル操作は例外の温床

ファイルの読み書きは、

  • ファイルがロックされている
  • 権限がない
  • ディスクがいっぱい

など、いろいろな理由で例外が発生しやすい領域です。

そこで、SaveToFileLoadFromFile に、 軽い例外処理を足してみましょう。

public void SaveToFile(string filePath)
{
    try
    {
        var options = new JsonSerializerOptions
        {
            WriteIndented = true
        };

        string json = JsonSerializer.Serialize(_tasks, options);

        File.WriteAllText(filePath, json);
    }
    catch (Exception ex)
    {
        // 学習用なので、ここでは簡単にメッセージを表示するだけにします
        Console.WriteLine("タスクの保存中にエラーが発生しました。");
        Console.WriteLine($"詳細: {ex.Message}");
    }
}

public void LoadFromFile(string filePath)
{
    try
    {
        if (!File.Exists(filePath))
        {
            return;
        }

        string json = File.ReadAllText(filePath);

        var tasks = JsonSerializer.Deserialize<List<TaskItem>>(json);

        if (tasks == null)
        {
            Console.WriteLine("タスクデータの読み込みに失敗しました。JSONの形式が不正な可能性があります。");
            return;
        }

        _tasks.Clear();
        _tasks.AddRange(tasks);

        if (_tasks.Any())
        {
            _nextId = _tasks.Max(t => t.Id) + 1;
        }
        else
        {
            _nextId = 1;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("タスクの読み込み中にエラーが発生しました。");
        Console.WriteLine($"詳細: {ex.Message}");
    }
}
C#

セキュリティ・安全性の視点

  • 実際のプロダクションコードでは、 例外の内容をそのままユーザーに見せるのではなく、 ログに記録して、ユーザーには簡潔なメッセージだけを見せることが多いです。
  • 学習段階では、ex.Message を表示しておくことで、 「何が起きたのか」を理解する助けになります。

Day 79 用ミニテンプレート:入力チェック+例外処理入り

最後に、Day 79 の内容を反映した、 「入力チェックと例外処理を強化したタスク管理アプリ」のミニテンプレートを載せておきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.IO;

class TaskItem
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public int Priority { get; set; }
    public DateTime? DueDate { get; set; }
    public bool IsCompleted { get; set; }
    public DateTime CreatedAt { get; set; }
}

class TaskRepository
{
    private readonly List<TaskItem> _tasks = new List<TaskItem>();
    private int _nextId = 1;

    public TaskItem Add(string title, string description, int priority, DateTime? dueDate)
    {
        var task = new TaskItem
        {
            Id = _nextId++,
            Title = title,
            Description = description,
            Priority = priority,
            DueDate = dueDate,
            IsCompleted = false,
            CreatedAt = DateTime.Now
        };

        _tasks.Add(task);
        return task;
    }

    public IEnumerable<TaskItem> GetAll()
    {
        return _tasks;
    }

    public TaskItem FindById(int id)
    {
        return _tasks.FirstOrDefault(t => t.Id == id);
    }

    public bool Remove(int id)
    {
        var task = FindById(id);
        if (task == null)
        {
            return false;
        }

        _tasks.Remove(task);
        return true;
    }

    public void SaveToFile(string filePath)
    {
        try
        {
            var options = new JsonSerializerOptions
            {
                WriteIndented = true
            };

            string json = JsonSerializer.Serialize(_tasks, options);

            File.WriteAllText(filePath, json);
        }
        catch (Exception ex)
        {
            Console.WriteLine("タスクの保存中にエラーが発生しました。");
            Console.WriteLine($"詳細: {ex.Message}");
        }
    }

    public void LoadFromFile(string filePath)
    {
        try
        {
            if (!File.Exists(filePath))
            {
                return;
            }

            string json = File.ReadAllText(filePath);

            var tasks = JsonSerializer.Deserialize<List<TaskItem>>(json);

            if (tasks == null)
            {
                Console.WriteLine("タスクデータの読み込みに失敗しました。");
                return;
            }

            _tasks.Clear();
            _tasks.AddRange(tasks);

            if (_tasks.Any())
            {
                _nextId = _tasks.Max(t => t.Id) + 1;
            }
            else
            {
                _nextId = 1;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("タスクの読み込み中にエラーが発生しました。");
            Console.WriteLine($"詳細: {ex.Message}");
        }
    }
}

class Program
{
    static TaskRepository repository = new TaskRepository();
    const string TaskFilePath = "tasks.json";

    static void Main()
    {
        try
        {
            repository.LoadFromFile(TaskFilePath);
            Run();
            repository.SaveToFile(TaskFilePath);
        }
        catch (Exception ex)
        {
            Console.WriteLine("予期しないエラーが発生しました。アプリを終了します。");
            Console.WriteLine($"詳細: {ex.Message}");
        }
    }

    static void Run()
    {
        while (true)
        {
            Console.WriteLine("=== タスク管理アプリ ===");
            Console.WriteLine("1: タスク追加");
            Console.WriteLine("2: タスク一覧");
            Console.WriteLine("0: 終了");
            Console.Write("選択してください: ");

            string input = Console.ReadLine();

            if (string.IsNullOrWhiteSpace(input))
            {
                Console.WriteLine("入力が空です。メニュー番号を入力してください。");
                Console.WriteLine();
                continue;
            }

            switch (input)
            {
                case "1":
                    CreateTask();
                    break;
                case "2":
                    ShowTasks();
                    break;
                case "0":
                    Console.WriteLine("終了します。");
                    return;
                default:
                    Console.WriteLine("不正な入力です。メニューに表示されている番号を入力してください。");
                    break;
            }

            Console.WriteLine();
        }
    }

    static int ReadInt(string label)
    {
        while (true)
        {
            Console.Write($"{label}を入力してください: ");
            string text = Console.ReadLine();

            if (string.IsNullOrWhiteSpace(text))
            {
                Console.WriteLine("入力が空です。数値を入力してください。");
                continue;
            }

            if (int.TryParse(text, out int value))
            {
                return value;
            }

            Console.WriteLine("数値として認識できませんでした。もう一度入力してください。");
        }
    }

    static DateTime? ReadDateOrNull(string label)
    {
        while (true)
        {
            Console.Write($"{label}を入力してください (yyyy-MM-dd、未入力なら期限なし): ");
            string text = Console.ReadLine();

            if (string.IsNullOrWhiteSpace(text))
            {
                return null;
            }

            if (DateTime.TryParse(text, out DateTime date))
            {
                return date;
            }

            Console.WriteLine("日付の形式が正しくありません。例: 2026-09-10 のように入力してください。");
        }
    }

    static void CreateTask()
    {
        Console.WriteLine("=== タスク追加 ===");

        Console.Write("タスク名を入力してください: ");
        string title = Console.ReadLine();
        if (string.IsNullOrWhiteSpace(title))
        {
            Console.WriteLine("タスク名は必須です。空では登録できません。");
            return;
        }

        Console.Write("詳細(メモ)を入力してください(空でも構いません): ");
        string description = Console.ReadLine();

        int priority = ReadInt("優先度 (1: 高, 2: 中, 3: 低)");

        DateTime? dueDate = ReadDateOrNull("期限");

        var task = repository.Add(
            title,
            description ?? string.Empty,
            priority,
            dueDate
        );

        Console.WriteLine($"タスクを追加しました。ID: {task.Id}");
    }

    static void ShowTasks()
    {
        Console.WriteLine("=== タスク一覧 ===");

        var tasks = repository.GetAll().ToList();

        if (!tasks.Any())
        {
            Console.WriteLine("タスクが登録されていません。");
            return;
        }

        foreach (var t in tasks)
        {
            string status = t.IsCompleted ? "[完了]" : "[未完了]";
            string priorityText = t.Priority switch
            {
                1 => "高",
                2 => "中",
                3 => "低",
                _ => "不明"
            };
            string dueText = t.DueDate.HasValue
                ? t.DueDate.Value.ToString("yyyy-MM-dd")
                : "期限なし";

            Console.WriteLine($"ID: {t.Id}");
            Console.WriteLine($"  状態   : {status}");
            Console.WriteLine($"  優先度 : {priorityText}");
            Console.WriteLine($"  期限   : {dueText}");
            Console.WriteLine($"  タイトル: {t.Title}");
            Console.WriteLine($"  詳細   : {t.Description}");
            Console.WriteLine();
        }
    }
}
C#

Day 79 のまとめ

Day 79 では、タスク管理アプリを 「不正な入力でも落ちないプログラム」 に近づけました。

  • 例外とは何かをイメージで理解し
  • try / catch の基本形を押さえ
  • int.TryParseDateTime.TryParse を使った入力チェックを強化し
  • メインループやJSON読み書きに例外処理を追加して、 予期せぬエラーでも丁寧に終了できるようにしました。

これで、アプリはだいぶ「頼れる相棒」になってきました。

コードを書きながら、

「ユーザーがどんな変な入力をしても、できるだけ優しく受け止めたい」

という気持ちを少しずつ乗せていくと、 プログラムは自然と、使う人にとって心地よいものになっていきます。

Day 79 は、そんな「思いやりのあるコード」の第一歩です。 ぜひ、わざと変な入力をしてみて、 どんなメッセージが返ってくるかを確かめてみてください。 その一つひとつが、みなさんのプログラムを強く、優しくしていきます。

タイトルとURLをコピーしました