Tsuの雑記¯\_(ツ)_/¯

主に製作メモ・備忘録として使用。製作したアプリのリンクもあります。

【Unity】例外 throw の必要性【C#】

例外Exceptionを投げるthrowキーワードについて,その意義を改めて考えます。

(Unity 2018.3.0f2)


始めに

今回は,以下の記事中にあるスクリプトを例として御説明いたします。

tsu-games.hatenablog.com


スクリプトの用意

throwキーワードは2か所にありますが,上から順に見て行きましょう。

Try-Catch キーワードを使う場合

LastElement.cs

// 例外が生じるおそれのある命令
try
{
    // 指定したインデックスの要素を返す
    return objects[LastIndexNumberOf(objects) + shift];
}
// 引数の例外をキャッチした場合
catch (ArgumentOutOfRangeException)
{
    // 例外を投げる
    throw new ArgumentOutOfRangeException(nameof(shift), ShiftExceptionMessage);
}

1つ目のthrowキーワードは,catchキーワード内で使われています。

これはtryキーワードと組み合わせて使う物で,tryキーワード内で起こり得る例外を拾う役割が有ります。

docs.microsoft.com

それでは,ここで起こり得る例外とは何でしょうか。

それは,object[]配列のインデクサー内に有るshiftパラメータです。

配列のインデクサーは「0以上,(配列の要素数+1)以下」の整数を指定する必要が有りますが,ユーザーの入力したshiftの値によっては範囲外になってしまうおそれが有ります。

そこで,引数が範囲外だった場合の例外ArgumentOutOfRangeExceptioncatchキーワードで拾います。

docs.microsoft.com

しかし,そもそもインデクサーが範囲外だった場合の例外ArgumentOutOfRangeExceptionは自動的に投げられる物です。

なぜ,改めて自分で用意する必要が有るのでしょうか。

実験として,先程の抜粋部分を以下のように書き換えてみてください。

LastElement.cs

// 指定したインデックスの要素を返す
return objects[LastIndexNumberOf(objects) + shift];

try-catchキーワードを省く事で,例外処理をしないよう変更しました。

ここで,次のようなスクリプトを別途用意して実行してみましょう。

NewBehaviourScript.cs

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    void Start()
    {
        // 整数配列
        var ints = new int[] { 0, 1, 2 };
        Debug.Log(ints.GetLastElement(-3));
    }
}

素数が3の配列ints[]に対して「最後の要素から3つ前の要素」を指定しているため,引数が範囲外だった場合の例外ArgumentOutOfRangeExceptionが発生するはずですね。

UnityEditor の Console 画面には,次のように表示されたと思います。

Console 画面に ArgumentOutOfRangeException が表示されている画像
例外処理をせずとも例外はスローされたが……?

例外メッセージが表示されているので問題無いように見えますが,2行目に御注目ください。

Parameter name: indexと書かれていますね。

今回は拡張メソッドを作るのも使うのも私(あなた)なので,この例外メッセージの意味が分かります。

しかし実際にこのメッセージを読む事になるユーザーは,拡張メソッドの中身を知りません。

この拡張メソッドが仮にオープンソースであれば解決は出来るでしょうが,手間が掛かります。

ましてや非公開ソースやアセットともなれば,このメッセージを読み解くのは難しいでしょう。

ですからthrowキーワードを使う事で,ユーザーの為に例外を分かりやすく書き換える必要が有るわけです。

docs.microsoft.com

先程のLastElement.csを元に戻して再度実行すると,下図のように表示されると思います。

UnityEditor の Console 画面に独自の例外メッセージが表示されている画像
例外処理によって独自のメッセージが表示される

これで,よりユーザーに親切なコードとなりました。

Try-Catch キーワードを使わない場合

2つ目のthrowキーワードでは,try-catchキーワードを使わずにif分岐で例外を拾っています。

LastElement.cs

// 要素が1つもない場合
if (count < 1)
{
    // メソッド呼び出しの例外を投げる
    throw new InvalidOperationException(CountExceptionMessage);
}

// 末尾インデックス番号をリターン
return (count - 1);

このcountの値によって実際に例外が発生するのは先程のGetLastElement()メソッド内であるため,より早い段階で例外を投げるようにしています。

こちらも実験として,先程の抜粋部分を以下のように書き換えてみてください。

LastElement.cs

// 末尾インデックス番号をリターン
return (count - 1);

if部を省く事で,例外処理をしないよう変更しました。

ここで,先程のNewBehaviourScript.csを以下のように書き換えて実行してみましょう。

NewBehaviourScript.cs

using UnityEngine;
// 拡張メソッド GetLastElement() を使うため
using static LastElement;

public class NewBehaviourScript : MonoBehaviour
{
    void Start()
    {
        // 整数配列
        var ints = new int[] { };
        Debug.Log(ints.GetLastElement());
    }
}

素数が0の配列ints[]に対して「最後の要素」を指定しているため,メソッド呼び出しの例外InvalidOperationExceptionをスローしてほしい所です。

docs.microsoft.com

UnityEditor の Console 画面には,次のように表示されたと思います。

Console 画面に InvalidOperationException が表示されている画像
例外の名前に注目

1つ目の例に同じくParameter name: indexと書かれているのも問題ですが,そもそも例外の種類がInvalidOperationExceptionではなくArgumentOutOfRangeExceptionになってしまいました。

内部的にはインデクサーを伴うobjects[LastIndexNumberOf(objects) + shift]という処理を行っているため,本来はこの例外メッセージが正しいのです。

しかしユーザーは引数を指定しないints.GetLastElement()を呼び出しているつもりなので,この例外メッセージは解決のヒントになるどころか惑わせてしまいますよね。

LastElement.csを元に戻して再度実行すると,下図のように表示されると思います。

UnityEditor の Console 画面に独自の例外メッセージが表示されている画像
例外処理によって独自のメッセージが表示される

これで,よりユーザーに親切なコードとなりました。


終わりに

例外の種類は今回扱った物だけではありませんので,その他の例外に関しては以下のリンク先などを御参照ください。

docs.microsoft.com

チームで開発する場合にも適切なメッセージを出す事は重要ですし,自分の製作物でも時間が経てば分かりにくくなるものです。

「いつ,誰が見ても分かる」という事を意識して,製作したいものですね。

以上,throwキーワードの必要性に関する記事でした。