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読み書きにも例外処理を足してみる
ファイル操作は例外の温床
ファイルの読み書きは、
- ファイルがロックされている
- 権限がない
- ディスクがいっぱい
など、いろいろな理由で例外が発生しやすい領域です。
そこで、SaveToFile と LoadFromFile に、 軽い例外処理を足してみましょう。
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.TryParseやDateTime.TryParseを使った入力チェックを強化し- メインループやJSON読み書きに例外処理を追加して、 予期せぬエラーでも丁寧に終了できるようにしました。
これで、アプリはだいぶ「頼れる相棒」になってきました。
コードを書きながら、
「ユーザーがどんな変な入力をしても、できるだけ優しく受け止めたい」
という気持ちを少しずつ乗せていくと、 プログラムは自然と、使う人にとって心地よいものになっていきます。
Day 79 は、そんな「思いやりのあるコード」の第一歩です。 ぜひ、わざと変な入力をしてみて、 どんなメッセージが返ってくるかを確かめてみてください。 その一つひとつが、みなさんのプログラムを強く、優しくしていきます。
