13日目のゴールとテーマ
13日目のテーマは「エラーに強いアプリにする(例外処理とメニュー化)」です。
ここまでで、機能としてはかなり“それっぽい”アプリになってきましたが、まだ弱いところがあります。
ファイルが壊れていたら?
数字を入れてほしいところに文字を入れられたら?
ユーザーが変な選択をしたら?
今日はここに踏み込んで、
例外処理(try / catch)で落ちないコードにする
ユーザーにメニューを見せて「やりたいことを選んでもらう」
入力ミスに優しく対応する
という「現実世界で動けるアプリ」に近づけていきます。
例外とは何かをイメージでつかむ
「普通じゃない事態」が起きたときのシグナル
C# では、プログラムが想定していない事態が起きたときに「例外(Exception)」が投げられます。
存在しないファイルを読み込もうとした
数字に変換できない文字列を int.Parse した
0 で割り算した
こういうとき、何もしないとプログラムは「未処理の例外」で落ちます。
例外は「やばいことが起きたから、どうするか決めてくれ」というシグナルです。
try / catch の基本形
例外をキャッチして、自分で対処するのが try / catch です。
try
{
// 例外が起きるかもしれない処理
}
catch (Exception ex)
{
// 例外が起きたときの処理
}
C#イメージとしては、
このブロックの中で何かトラブルが起きたら、
落ちる代わりに catch に飛んでくる
という感じです。
ファイル読み込みに例外処理を入れる
File.ReadAllLines は失敗することがある
今までは、こう書いていました。
string[] lines = File.ReadAllLines(fileName);
C#ファイルが存在しない場合は File.Exists で守っていましたが、
例えば「アクセス権がない」「ファイル名が異常に長い」など、
他の理由で失敗することもあります。
そこで、Repository に try / catch を入れてみます。
public List<Result> LoadAll()
{
List<Result> list = new List<Result>();
if (!File.Exists(_logFileName))
{
return list;
}
try
{
string[] lines = File.ReadAllLines(_logFileName);
foreach (string line in lines)
{
Result r = Result.ParseFromLogLine(line);
if (r != null)
{
list.Add(r);
}
}
}
catch (Exception ex)
{
Console.WriteLine("ログの読み込み中にエラーが発生しました。");
Console.WriteLine("詳細: " + ex.Message);
}
return list;
}
C#ここでの重要ポイントはこうです。
File.ReadAllLines 全体を try の中に入れている
何か問題が起きても、アプリ全体は落ちずに「エラーが出た」と表示して続行できる
例外の詳細(ex.Message)を表示しておくと、原因調査に役立つ
「エラーを完全に消す」のではなく、
「エラーが起きても落ちないようにする」という発想です。
数値入力に対して安全に対応する
int.Parse は失敗すると例外を投げる
ユーザーに数字を入力してもらう場面を考えます。
Console.WriteLine("番号を入力してください:");
string input = Console.ReadLine();
int number = int.Parse(input);
C#ここでユーザーが「abc」と入力すると、int.Parse は例外を投げて落ちます。
これを避けるために、int.TryParse を使います。
int.TryParse の基本
bool ok = int.TryParse(input, out int number);
C#TryParse は、
変換に成功したら true を返し、number に値が入る
失敗したら false を返し、number には 0 が入る
という「失敗しても例外を投げない」安全な変換です。
メニュー選択で TryParse を使う
13日目では、メニューを数字で選んでもらう形にします。
static int ReadMenuNumber()
{
while (true)
{
Console.Write("番号を入力してください: ");
string input = Console.ReadLine();
if (!int.TryParse(input, out int number))
{
Console.WriteLine("数字で入力してください。");
continue;
}
if (number < 0 || number > 3)
{
Console.WriteLine("0〜3 の範囲で入力してください。");
continue;
}
return number;
}
}
C#ここでの重要ポイントはこうです。
TryParse で「数字かどうか」をチェックしている
範囲チェックも一緒に行っている
正しい入力が来るまで while(true) で聞き続ける
これで、ユーザーがどんな文字を入れても、
アプリが落ちることはなくなります。
メニューを持ったアプリにする
「やりたいことを選ぶ」メニューを作る
ここまで来たら、アプリ起動時にメニューを出して、
ユーザーに選んでもらう形にしましょう。
例えば、こんなメニューです。
0: 終了
1: 最近の履歴を見る
2: タイプ別件数を見る
3: 新しい診断を行う
これをコードにするとこうなります。
static void Main(string[] args)
{
var repo = new ResultRepository("log.txt");
while (true)
{
Console.WriteLine("=== メニュー ===");
Console.WriteLine("0: 終了");
Console.WriteLine("1: 最近の履歴を見る");
Console.WriteLine("2: タイプ別件数を見る");
Console.WriteLine("3: 新しい診断を行う");
Console.WriteLine();
int choice = ReadMenuNumber();
if (choice == 0)
{
Console.WriteLine("終了します。");
break;
}
if (choice == 1)
{
HandleShowRecent(repo);
}
else if (choice == 2)
{
HandleShowSummary(repo);
}
else if (choice == 3)
{
HandleNewDiagnosis(repo);
}
Console.WriteLine();
}
}
C#ここでのポイントはこうです。
Main は「メニューを出す」「選択を受け取る」「ハンドラを呼ぶ」だけ
実際の処理は Handle〜 メソッドに分ける
while(true) でメニューを繰り返し表示し、0 が選ばれたら break
「メニューを持つアプリ」というだけで、
一気に“アプリ感”が増します。
メニューごとの処理を分ける
最近の履歴を見る処理
static void HandleShowRecent(ResultRepository repo)
{
var results = repo.LoadAll();
var service = new ResultService(results);
service.ShowRecent(5);
}
C#タイプ別件数を見る処理
static void HandleShowSummary(ResultRepository repo)
{
var results = repo.LoadAll();
var service = new ResultService(results);
service.ShowTypeSummary();
}
C#新しい診断を行う処理
static void HandleNewDiagnosis(ResultRepository repo)
{
int yesCount = RunDiagnosis();
Result newResult = CreateResultFromYesCount(yesCount, 5);
try
{
repo.Append(newResult);
Console.WriteLine("診断結果を保存しました。");
}
catch (Exception ex)
{
Console.WriteLine("診断結果の保存中にエラーが発生しました。");
Console.WriteLine("詳細: " + ex.Message);
}
}
C#ここでの重要ポイントはこうです。
メニューごとに「やること」を小さなメソッドに分けている
ResultRepository と ResultService を毎回組み立てて使っている
保存時にも try / catch を入れて、失敗しても落ちないようにしている
Main が「全体の流れ」、
Handle〜 が「メニューごとの処理」、
Repository / Service / Result が「中身のロジック」
という役割分担が、よりハッキリしてきました。
13日目のまとめ
今日のキーワードを整理します。
例外(Exception)
想定外の事態が起きたときに投げられるシグナル。
何もしないとアプリは落ちる。
try / catch
「ここでトラブルが起きたら、こう対処する」と決める仕組み。
落とさずにメッセージを出して続行できる。
int.TryParse
文字列を安全に数値に変換する。
失敗しても例外を投げず、true / false で結果を返す。
メニュー構造
while(true) でメニューを表示し、
ユーザーの選択に応じて処理を分岐する。
ハンドラメソッド
HandleShowRecent のように、「メニュー1つ分の処理」を
小さなメソッドに分けると、Main が読みやすくなる。
ここまで来ると、
「動くだけのサンプル」から
「エラーにもある程度耐えられる、小さな実用アプリ」
にかなり近づいています。
