BACK INDEX NEXT

Cocoaの例外処理

多くの方の助けをいただいて、最初にかなり重い話題を扱うことができ、ほっとしています。さて、今回選んだテーマはまたもや、Objective-Cのみの話題で、Cocoaの例外処理です。

そもそも例外とは何か

例外とは、プログラムの実行において例外的に発生した事象であり、例外処理とは、その例外的な事象が起こった時にそれを捕まえて、適切な処理を施すことです。適切な処理というのは、エラーパネルの表示であったり、事象の無視であったり、プログラムの終了であったりと一概に言えませんが、そういう事象を捉えるという点に意味があります。ところで、例外的な事象にはプログラムで回避不可能なものと、回避可能なもの(JavaにおけるRuntimeExceptionがこれに相当)が存在します。ものの本によれば、前者は例外として捉えるべき意味があり、後者はむしろプログラムのバグであるということです。CocoaにおけるObjective-Cの例外も、基本的にそのスタンスにたった実装になっています。このあたりは、実行システムの違いもあるので一概に比較できませんが、Javaがおよそすべての例外的な事象を捉えることができるのとは、趣を異にしています。特にメモリの不正アクセスに対する強さは、Javaのほうが優れているといわざるを得ませんが、それは例外ではなく、(設計または実装上の)バグだという認識を持ちたいものです。

例外の生成

Objective-Cにおける例外の生成は、NSExceptionのオブジェクトにより行われます。以下の例では、resultが-1だった時に、@"FooException"という名前を持つ例外をあげています。

- (void)foo
{
    if (result == -1) {
        [[NSException exceptionWithName:@"FooException"
                                 reason:@"Result returned -1.."
                               userInfo:nil]
            raise];
    }
}

この例から類推できるように、CocoaのObjective-C環境における例外クラスはNSExceptionだけで、Javaがさまざまなサブクラスによって例外を区別しているのとは、趣が違います。どの例外があがったかを知るには、NSExceptionに設定された例外名などの情報を判定する必要があります。システムであらかじめ定義された例外名には、以下のものをはじめいろいろあるので、判定にはそれを使うこともできます。また、自分で例外名を定義する場合は、頭に"NS"をつけるのは止めましょう。

NSGenericException
NSRangeException
NSInvalidArgumentException
NSMallocException

例外のハンドリング

Objective-Cにおける例外のハンドリングは、NS_DURING〜NS_HANDLER〜NS_ENDHANDLERによって構成されます。以下の例は、先ほどのメソッドからあがってきた例外をハンドリングして例外名をプリントした後、正常コードに復帰するものです。この例でのポイントは、ハンドリングした例外は、必ずlocalExceptionという変数名で参照されるという点です。

- (void)bar
{
    NS_DURING
        [self foo];
    NS_HANDLER
        printf("%s\n", [[localException name] cString]);
    NS_ENDHANDLER

    printf("Here is normal.\n"); // 例外があがった場合も実行される
}

ところで、barで例外をハンドリングしないとどうなるかというと、barを呼び出したメソッドに例外が渡っていきます。これはちょうど、Javaにおけるthrows Exception指定メソッドと同様の動きになります。

ハンドラ内でのreturn

例外をハンドリングして、メソッドを終了し、呼び出し元に制御を返したい場合があります。そのような場合のために、以下の二つのマクロが定義されています。

NS_VOIDRETURN
NS_VALUERETURN()

例外をハンドリングしたメソッドが戻り値を必要としないならNS_VOIDRETURNを、戻り値が必要な場合はNS_VALUERETURN()を使用することができます。以下には、NS_VALUERETURN()を使用した例を示します。NS_VALUERETURN()マクロに与える引数は、値と型名です。これらのマクロの展開は、プリプロセッサによって処理されるので、オブジェクトの場合も、同じようにNS_VALUERETURN(@"Error", NSString *)などと書けばOKです。

- (int)bar
{
    NS_DURING
        [self foo];
    NS_HANDLER
        NS_VALUERETURN(-1, int);
    NS_ENDHANDLER
}

例外ハンドリングのネスト

Objective-Cにおける例外ハンドリングは、ネストすることができます。以下の例は、ハンドリングした例外を、単に上に返しているものです。ここで単に返さずに、別な例外を返すこともできます。

- (void)foo
{
    NS_DURING
        [self bar];
    NS_HANDLER
        printf("%s\n", [[localException name] cString]);
    NS_ENDHANDLER

    printf("Here is normal.\n");
}

- (void)bar
{
    NS_DURING
        [self baz];
    NS_HANDLER
        [localException raise];
    NS_ENDHANDLER

    printf("Here is normal.\n"); // ここは例外時には実行されない
}

- (void)baz
{
    if (result == -1) {
        [[NSException exceptionWithName:@"BazException"
                                 reason:@"Result returned -1.."
                               userInfo:nil]
            raise];
    }
}

捕捉されなかった例外のハンドリング

さてスレッドの途中で捕捉されなかった例外は、最終的に実行環境に用意されたデフォルトの例外ハンドラがこれを捕まえて、メッセージを出します。通常のアプリケーションではその後、イベントループに戻るのでそのまま動くことが多いと考えられますが、基本的にこの状態はバグであると考えてよいでしょう。さて、このデフォルトのハンドラを別なものと置き換えるための、APIが用意されています。

void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler *handler);

この関数の使い方は、(たぶん)以下のような感じだと思うんですが、うまく制御できません。ローカルでNSGetUncaughtExceptionHandler()(localHException)を呼ぶ分には、上手くいくんですが、、、。

void myHandler(NSException* exception)
{
    printf("uncaught %s\n", [[exception name] cString]);
}

...

    NSSetUncaughtExceptionHandler(*myHandler);

例外のデバッグ

標準のデバッガであるgdbで、例外のデバッグを行う方法を教えていただいたので紹介したいと思います。gdbでは、例外発生個所にブレークポイントを設定することができます。ただし、この設定はプログラムを走らせた後でないと設定できないので、main関数の先頭などにあらかじめダミーのブレークポイントを設定して、プログラムを走らせ、ブレークポイントでgdbに制御が戻った時に設定します。gdbのコマンドラインから、いかに示すコマンドを打ち込むことで、以降、例外発生時にgdb側に制御が戻るようになります。この時にスタック状態を確認することで、プログラムのどこで例外が発生したのかを容易に知ることができるようになります。

(gdb) b raise
【解説】例外にブレークポイントを設定する
[0] cancel
[1] all

Non-debugging symbols:
[2]    -[NSException raise]
[3]    raise
> 2
【解説】候補の中から、[2]を選択する
Breakpoint 2 at 0x9aaed7bc
(gdb) continue
【解説】処理続行

Breakpoint 2, 0x9aaed7bc in -[NSException raise] ()
(gdb)
【解説】例外発生時にブレークしたところ

◆この稿の執筆にあたり、以下の方に感謝いたします。ありがとうございます。
・高橋 究さん <kiwamu@zephyr.sato-hosp.or.jp>
BACK INDEX NEXT