C# | 2週間で身につくアプリを作りながら学ぶC#の基本 - 13日目

C# C#
スポンサーリンク

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 が読みやすくなる。

ここまで来ると、
「動くだけのサンプル」から
「エラーにもある程度耐えられる、小さな実用アプリ」
にかなり近づいています。

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