プログラミング基礎講座番外編

ゼロから始めるプログラミング基礎講座

実行時エラーに対処する「try-catch」

講座全体の概要

プログラミング基礎講座の番外編です。プログラム内で発生したエラーに対応するために使用するtry-catch文というものについて解説します。講座全体の概要は以下の記事ゼロから始めるプログラミング基礎講座 第1回【講座の概要・目的】をご覧ください。

エラー処理とは

プログラムの実行時にエラーが発生し、異常終了してしまうことがあります。異常終了とは、エラーによりプログラムが中断されて、想定外の状態でプログラムが強制的に終了してしまうことを言います。

例えば、割り算をするプログラムで割る数を変数としている場合に、変数が0の時にはエラーが発生してしまいます。このエラーへの対処方法は2種類あります。

1つ目は、割り算を行う前にチェックする方法です。変数が画面入力によって直接決まるのであれば、0が入力されることは十分想定できますので、入力内容をチェックして、0が入力されたら正しい入力を促す様なエラーメッセージを表示するべきです。この様に画面等からユーザーが入力するデータや、プログラムの外部から渡されるデータ等は、基本的に事前のチェック処理を行います。

2つ目は、エラーが発生したことを捕捉してプログラムを想定した状態に戻し、異常終了させないようにする方法です。複雑な計算過程で割り算が行われており、事前のチェックが難しい場合にこちらの方法をとります。この様に、エラーの発生を捕捉して事後処理することをエラー処理と言います。特に、あらかじめ用意されているメソッド(関数)を使用する際に、事前のチェックは難しいけれど、使用したメソッド内でエラーが発生する可能性がある場合にこちらの方法を用います。

今回は2つ目に挙げたエラー処理を行うtry-catch文を解説します。

(余談ですが、1つ目の例で、ブラウザーの画面上で0を入力できないようにすることも可能です。しかし、Webアプリケーションの場合、画面上の入力制限やチェックとは別に、必ずサーバー側でも入力チェックを行うことが必要です。理由の詳細は省きますが、Webアプリケーションでは、ユーザーが不正な操作をしてサーバーへデータを送信することが可能なためです。)

Exception(例外)とは

C#を含む多くのプログラミング言語では、プログラム内で発生するエラーのことをException(読み方:エクセプション)と言います。日本語では例外と言います。なので、エラー処理のことは、例外処理とも言います。言語のリファレンス(ヘルプ)でも例外処理と書かれていますので以降では例外処理と呼びます。

Exception(例外)は、その発生した原因によっていくつもの種類に分けられます。以下によく発生するものを例として挙げて、その対処方法の例も示します。

NullReferenceException

参照型の変数に値が設定されていない状態(null)で、そのメンバー(プロパティやメソッド)にアクセス(参照)した場合に発生する例外。new クラス名()等で変数の初期化がきちんとされているか確認する。

IndexOutOfRangeException

配列やList等の要素へのアクセスにインデックス(添字)を指定した際に、そのインデックスが要素数以上の値を指している場合に発生する例外。インデックスが変数の場合は、プログラムに誤りがないか確認する。インデックスが固定値(0や1等)の場合は、要素数を事前にチェックする。また、全要素に繰り返し処理をするのであれば、foreach文への置き換えを検討する。

FileNotFoundException

開こうとしたファイルが見つからない場合に発生する例外。指定したファイル名やフォルダパスが正しいか確認する。また、ファイルやフォルダ等の読み書きはエラーが発生する可能性があるため、必ず例外処理を行う。

DivideByZeroException

0で割り算をした場合に発生する例外。基本的には事前に入力チェックをして防ぐ。

FormatException

文字列をParse等で数値型や日時に変換する際に、文字列の書式が不正(数値への変換であれば半角数字以外が含まれている等)で変換できない場合に発生する例外。Parseの代わりにTryParse等を使用して入力チェックを行うか、try-catch文で例外処理を行う。TryParseの使用方法はこの後で説明します。

上記はC#の例なので、プログラミング言語によって名前は異なりますが、ほとんどの言語で同様の例外があります。

TryParseの使い方

数字(数を表す文字列)からint型へ変換する例

string 数字 = "画面から入力された数字等";
int 数値;

if (int.TryParse(数字, out 数値))
{
    //正常に変換できた場合の処理
}
else
{
    //変換できない場合の処理
}

TryParseの戻り値はbool型で、文字列を数値に変換できる場合はtrue、変換できない場合はfalseが返されます。第1引数は変換したい文字列、第2引数は変換後の数値を入れるための変数を指定します。なお、第2引数には、例にある様に引数の前にoutを指定する必要があります(指定しないと Visual Studio がエラーを表示して教えてくれます)。このTryParseの様な一部のメソッドでは、戻り値とは別の処理結果を出力するために、引数に指定した変数に結果を出力する仕組みが用意されている場合があります。引数にoutを指定するようにエラーが表示された場合はこれに該当しますので、空の変数を指定してください。

try-catch文を使用した例外処理の方法

ここからは、今回の本題である例外処理について詳しく見ていきます。「BasicCourseEx2」というプロジェクトで、画面で入力された2つの数の割り算をする単純なプログラムを作成します。

色々なエラーを発生させるために、ブラウザー画面での入力の制限やチェックは行っていません。また、サーバー側もチェックは行っていません。

以下の例と、その後の解説を読みながら実装してください。

Index.cshtml.csの抜粋

namespace BasicCourseEx2.Pages
{
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        [BindProperty]
        public string InputNumber1 { get; set; }
        [BindProperty]
        public string InputNumber2 { get; set; }

        public string CalculatedResult { get; set; } = string.Empty;

        // 以下の様にプロパティにTempData属性を指定すると、ページの再表示をしても、このプロパティが使われるまで初期化されない。
        [TempData]
        public bool HasError { get; set; }

        [TempData]
        public string ErrorMessage { get; set; }

        public void OnGet()
        {
            InputNumber1 = string.Empty;
            InputNumber2 = string.Empty;
        }

        public IActionResult OnPostBtnCalculateClick()
        {
            try
            {
                int num1 = int.Parse(InputNumber1);
                int num2 = int.Parse(InputNumber2);
                CalculatedResult = (num1 / num2).ToString();
                return Page();
            }
            catch (FormatException)
            {
                // 例外処理1
                WriteError("半角数字以外は入力できません。");
                return RedirectToPage();    // こうすると自分自身(Index.cshtml)のページを再表示(OnGetメソッドを実行)させられる。
            }
            catch (DivideByZeroException)
            {
                // 例外処理2
                WriteError("0で割ることはできません。");
                return RedirectToPage();
            }
            catch (Exception ex)
            {
                // 例外処理3
                WriteError(ex.Message);
                return RedirectToPage();
            }
            finally
            {
                // 例外が発生したかどうかに係わらず、必要な処理があればここで行う。必要なければfinallyブロック自体が記述不要。
                _logger.LogDebug("try-catch-finallyのテスト");
            }

            // ここ(try-catch-finallyの後)に処理を記述することも可能です。
            // ただし、今回はtryブロックでreturnしているのでここでは何も処理できません。
        }

        private void WriteError(String errorMessage)
        {
            HasError = true;
            ErrorMessage = errorMessage;
        }

    }
}

画面で入力した値を受け取るためのstring型のプロパティを2つ用意しています。(InputNumber1と2)

そして、「計算する」ボタンをクリックされた時の処理を、「OnPostBtnCalculateClick」メソッドに記述しています。ここで本来行いたい処理は以下の流れです。

①画面の入力値をint型へ変換。

②割り算を実行した結果をstring型に変換して、画面表示用のプロパティに設定。

③処理結果を画面に返す

この流れの中でいくつかの例外が発生する可能性があります。

tryブロック

例外が発生する可能性がある部分はtry { }で囲みます。この部分をtryブロックと言い、これで発生した例外を捕捉することが可能になります。tryブロックには本来行いたい処理をすべて記述します。

catchブロック

tryブロックに続いて記述するのがcatch (~Exception) { }で囲った、catchブロックです。ここでは、例外が発生した場合の処理、すなわち例外処理を記述する部分になります。

例にある様に、例外の種類ごとに分けて、複数のcatchブロックを記述することができます。「~Exception」の部分が例外の種類を表します。

tryブロック中に例外が発生した時点で、たとえ処理の途中であってもtryブロックは終了し、catchブロックに処理が移ります。catchブロックは上から順に評価されて、該当する例外処理が1つだけ実行されます。

上記例の例外処理3で指定したExceptionという種類は、全ての例外を総まとめにしたもので、それより上に記述したcatchブロックのいずれにも該当しない場合は、このブロックが処理されます。特に例外の種類ごとに例外処理を分ける必要がなければ、このExceptionのcatchブロックだけ記述すればOKです。

また、例外処理3ではcatch (Exception ex) { }と、例外の種類の後に「ex」と指定しています。この「ex」は関数の引数の様なもので、これを記述しておくと、発生した例外の内容がここに格納され、そのcatchブロックの中で使用することができます。名前は任意に付けられますが、慣例としてExceptionの頭文字の「e」や「ex」が用いられます。特に使用する必要がなければ、指定を省略することができます。(例では「ex.Message」でC#のシステムから出力されたエラーメッセージをそのまま取得して画面に表示させていますが、本来はシステムからのメッセージをそのまま表示するべきではありません。ユーザーにとっては全く意味のわからないメッセージになってしまいますし、本来は隠しておくべき機密情報まで表示されてしまう可能性があるからです。実際のシステムでは、ユーザーは見ることができない、エラーログ等を出力しておく場合等に用います。)

finallyブロック

catchブロックに続くのがfinally { }で囲まれた、finallyブロックです。ここでは例外発生の有無に係わらず、最後に必ず実行すべき処理があれば記述をします。特に処理が必要なければ、finallyブロックそのものが不要なので、省略できます。

RedirectToPage()とTempData属性

今回の例では、例外発生時には画面を初期状態で表示させるため、return RedirectToPage()という文で、自分自身のページを再表示させています。RedirectToPageは、引数にページ名を指定することで、ブラウザーに対してその指定したページを表示させるための命令を返します。ブラウザーはこれを受け取って、指定されたページのURL(サーバー)にGet要求をして、そのページに遷移します。この一連の流れをリダイレクトと言います。今回の例の様に引数を指定せずにリダイレクトさせると、自分自身(今回はIndex)に改めてGet要求がされて、サーバー側のOnGetメソッドが実行されます。リダイレクトするとサーバーはプロパティの状態等を引き継げないので、本来であれば、最初に画面を表示した時とまったく同じ表示がされます。ですが、今回はエラーメッセージを表示させたかったので、特別にエラーメッセージとその有無だけは引き継げる様にしています。それは、引き継ぎたいプロパティにTempDataという属性を付与することで可能となります。このTempDataはそのプロパティが次に使われるまでデータを保持することができます。リダイレクトして再表示する時に「Index.cshtml」で参照しているので、その時はエラーが表示されますが、再度計算すると、すでにプロパティは初期化されていることが確認できます。

この様に、RedirectToPage()メソッドでリダイレクトさせてページを初期化しつつ、TempData属性を使用して必要なデータは引き継ぐことができます。

ブラウザー画面でのエラー表示

Index.cshtml

@page "{handler?}"
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">割り算</h1>
    <form method="post">
        <div class="form-group form-row justify-content-center">
            <input asp-for="InputNumber1" class="form-control col-2" />
            <span class="col-1">÷</span>
            <input asp-for="InputNumber2" class="form-control col-2" />
        </div>
        <input asp-page-handler="BtnCalculateClick" type="submit" value="計算する" class="btn btn-primary mb-3" />
    </form>

    @{
        @* 計算結果表示部分 *@
        if (Model.CalculatedResult != string.Empty)
        {
            <p>計算結果は<span class="mx-2">@Model.CalculatedResult</span>です。</p>
        }

        @* エラー表示部分 *@
        if (Model.HasError)
        {
            <div class="alert alert-danger alert-dismissible fade show" role="alert">
                @Model.ErrorMessage
                <button type="button" class="close" data-dismiss="alert" aria-label="閉じる">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
        }


        @* エラー表示部分をJavaScriptのalert関数にすると以下の様になる。 *@
        @*
            if (Model.HasError)
            {
                <script>
                    var msg = "@Html.Raw(Model.ErrorMessage)";
                    alert(msg);
                </script>
            }
        *@
    }
</div>

ブラウザーの画面でエラーメッセージをポップアップ表示する(いわゆるメッセージボックスの表示)にはJavaScriptのalert関数を使用することが最も簡単です。しかし、これだとメッセージのタイトルに変なものが表示されたりして色々不便です。

今回はエラー表示の方法としてBootstrapのアラート表示機能を利用しています。詳細の説明は省きますが、詳しく知りたい方はBootstrapalertsでWeb検索すれば日本語のリファレンスページ等が表示されますのでそちらで確認してください。

この様にBootstrapを利用すると、必要なHTMLタグのclass属性にBootstrapで用意されているクラスを指定するだけで、複雑なJavaScriptやCSSを自分で記述することなく、比較的簡単に色々と便利な機能が実現できます。メッセージ表示の目的であれば、Toasts(トースト)も利用できます。(アラートに比べると少し面倒ですが、色々な表示ができます。)

ボタンクリック時のページURLとクエリ―ストリングについて

今回、Index.cshtmlの先頭行をいつもと少し変えています。

いつもは


@page

だけですが、今回は


@page "{handler?}"

としています。これにより、「計算する」ボタンクリック時のURLが変わります。ボタンクリック時の画面イメージをURL部分に注目して比較してみましょう。

@pageだけの場合
{handler?}を追加した場合

1つ目の画面だと、URLは「https://localhost:44389/」までで、その後ろの?以降はQueryStringというものです。日本語ではクエリ文字列、または英語をそのまま読んでクエリ―ストリングと言います。Web検索した際に、検索サイトのURLの後ろに、検索ワード等が羅列されるのと同じもので、URLで指定したサーバーにパラメーターとして渡すための記述です。

2つ目の画面は「https://localhost:44389/BtnCalculateClick」までがURLになり、クエリ―ストリングはありません。クエリ―ストリングを使用したくない場合には、こちらの方法を使います。

深くは触れませんが、セキュリティ上の観点から、クエリ―ストリングは本当に必要な場面以外では使用したくないという意識があるため、今回はこの様にしました。実際のところは、今回のプログラムにクエリ―ストリングを使用しても特にリスクがあるわけではありませんが、私自身、常に気を配るように意識付けるためにも今回はURLに変更しています。

例外処理の制御を理解する

作成したプログラムをデバッグ実行して、色々と試してみると、言葉で説明されただけではわからなかったことが理解できるのではないかと思います。

今回のプログラムだと、以下のケースで例外が発生するはずです。

・何も入力せずに計算した場合

・半角数字以外を入力して計算した場合

・2つ目の入力欄を0として計算した場合

また、1つ目の入力だけ正しい場合や、両方の入力が正しい場合、間違っている場合などを組み合わせて試してください。

次回は総仕上げの演習問題

今回は以上となります。次回は総仕上げとして演習問題にチャレンジしましょう。次回の記事では最低限のヒントを掲載する予定です。

プログラミング基礎講座番外編使えると便利でバグのリスクも減らせる「Enum」の使い方前のページ

演習:アプリを作る。課題内容とプログラミングの進め方を紹介。次のページプログラミング基礎講座第9回

コメント
  1. この記事へのコメントはありません。

  1. この記事へのトラックバックはありません。

CAPTCHA


ピックアップ

  1. 4Kディスプレイで作業効率UP #3
  2. 4kdisplay for video

最近の記事

  1. recommended pc
  2. おすすめ動画配信サービス3選
  3. 4kdisplay for video
  4. オブジェクト指向の基本概念
  5. basic-knowledge-of-web-app
PAGE TOP