Re: nil に対するメッセージ送信が例外にならない

お題:

nil に対するメッセージ送信が例外にならない

この仕様って、Objective-C以外でほとんど見かけたことがないのだけど、メリットに比べてデメリットが大き過ぎると思う。オブジェクトのメソッドチェインでこの仕様がたまに便利なことはあるけど、ほとんどの場合、バグが発現するタイミングが遅くなるだけに終わるというのが経験則。この辺、熟練のObjective-Cerはどう考えてるのか一度知りたいところ。

Objective-Cで不満に思うこと - kmizuの日記

もう10年以上仕事でObjective-Cは使ってないので、熟練というよりは只のロートルなんだけど、自分なりに考えた事を書いてみようと思う。

随分前の事を思い出しながら書くのと、憶測も交えているので、あまり正確な内容でない事はあらかじめ断っておきます。

Objective-Cの例外について

まず、そもそもObjective-Cのプロパーな例外って存在しないんじゃないかという話をしたいと思う。

Mac OS XiOS用にObjective-Cでプログラムを書く際に良く出てくる例外って、だいたいNSExceptionクラスってやつだと思う*1。 NSExceptionという名前から分かるように、これはFoundationフレームワークの一員だったりする。

Foundation(Foundation Kit)は1995年にNEXTSTEP 3.3がリリースされた時に追加されたフレームワークで、(OpenStepという公開された仕様に基づくとはいえ) NeXT社が独自拡張したクラスライブラリであり、それ以前のObjective-CにはNSExceptionは存在しなかった。

じゃあそれまでObjective-Cに例外処理はなかったかというとそういうわけではなくて、NXHandlerという構造体とsetjmp/longjmpを使って実装された例外処理用のマクロが存在した。今のNS_DURING〜とほぼ同じような構文で使われていた。

NX_DURING
    /* code that may cause an error */
NX_HANDLER
    switch (NXLocalHandler.code)
    case 
        NX_someErrorCode:
            /* code to execute for this type of error */
    default: NX_RERAISE();
NX_ENDHANDLER

じゃあこのNX_DURING〜がObjective-Cのプロパーな例外処理機構というとそうでもなくて、名前から分かる通りNeXT社の独自機能だと思われる。少なくともListクラスやHashTableクラスのようなObjective-Cの基本クラスでは明示的には使われていない。

NeXT/Apple以外のObjective-C処理系ではどうだろうとおもってちょっと調べたけど、それぞれに別々の例外処理を持っているっぽい。例えば、Portable Object Compilerのマニュアルには次のように記載されている

This chapter discusses exception handling for OBJECTIVE-C as outlined in [Cox, 1991].

An exception -- in response to some abnormal program condition -- is raised by sending a error: message to an Object.

if (!h) [ anObject error:"h can't be zero" ];

This is similar to how in Stepstone OBJECTIVE-C an error action (aborting the process) is performed.

However, in our case, the user might substitute a different Block (called exception handler) for the default handler (which aborts the process). This is done by using the method ifError: :

[ { c = [a foo]; } ifError: { :msg :rcv | printf("got an exception"); } ];

Instead of evaluating the default handler, error: will execute the exception handler specified by ifError:. The handler is invoked with a message object and with the receiver of the error: message.

User Manual Portable Object Compiler Version 3.3.10

見るからに、かなり毛色が違っている。オブジェクトの-error:を呼ぶ事で例外を起こし、標準のハンドラではなくBlockを使って処理をおこなうらしい。

要するに言語系で異なっていて、本来のObjective-C的な例外というものはないと思われる。*2

nilの使われ方について

NEXTSTEP 3.3のマニュアルを見ていると、CommonクラスやApplicationKitのクラスで明示的に例外が使われているところはあんまりない*3。ストリームの読み書き系やフォント周り、NXColorなどのidではなく構造体を返す処理、DPS周りなどに少しある程度。それ以外は、例外を起こさずにnilを返しているように見える。

例えば、可変長のコレクションクラスであるListクラスの、指定した位置にオブジェクトを挿入するためのメソッドは、マニュアルに次のように書かれている。

insertObject:at:

- insertObject:anObject at:(unsigned int)index

Inserts anObject into the List at index, moving objects down one slot to make room. If index equals the value returned by the count method, anObject is inserted at the end of the List. However, the insertion fails if index is greater than the value returned by count or anObject is nil.

If anObject is successfully inserted into the List, this method returns self. If not, it returns nil.


List - Common Classes and Functions - NEXTSTEP General Reference (c) 1995 by NeXT Computer, Inc.

要素がnilの場合や、挿入箇所にリストの要素数より大きい数値を指定した場合、例外を起こさずにnilを返す。挿入が成功した場合はselfを返す。

このように、nilは単なる空値ではなく、明らかに失敗系として使われていたと思われる。

このことから推測するに、nilにメッセージを送るとnilを返すのは、失敗系の伝播という意味合いを担っていたのではないか。例えば、次のような架空のコードがあったとして、

    [[self itemList] addObject:[[Item createNewItem] prepareFor:self]];

処理中に、+createNewItemや-prepareFor:が失敗した時にもnilを返す事で、この行の処理全体を中断して抜け出せるように作ることが出来る。

つまり、便利だからメソッドチェーンというのではなくて、制御フローとして使う為のものだったのではないかという想像です。


Foundationフレームワークが導入されたあたりから、return selfするメソッドも減っていって、例えば現在のコレクションクラスであるNSMutableArrayの挿入メソッドは、次のように戻り値の型はvoidになり、異常時には例外を起こすようになっている。

insertObject:atIndex:

Inserts a given object into the array's contents at a given index.

- (void)insertObject:(id)anObject atIndex:(NSUInteger)index

(cut)
Important Raises an NSInvalidArgumentException if anObject is nil.
(cut)
Important Raises an NSRangeException if index is greater than the number of elements in the array.

NSMutableArray Class Reference

個人的には、Foundationに切り替わった段階で、nilにメッセージ送ると例外、という仕様に変わっても良かったような気もする。少なくとも[ [ [Hoge alloc] init] autorelease]などの特定のイディオム以外では、自分は積極的には使わないようになっていった。

追記: Portable Object Compilerでのnilの扱い

オプションで挙動を変えられるみたいですな。

Sending Messages to Nil

The -noNilRcvr option can be used to prevent messages being sent to nil (the NULL pointer). It is in fact a runtime option, since the only effect of this option is that, when the main() of the program is compiled with this option, a nilHandler() function will be registered that stops the process, instead of simply returning nil (as the default handler does).

objc -noNilRcvr main.m

User Manual Portable Object Compiler Version 3.3.10

*1:今はNSExceptionでなくてもスローキャッチできるらしいけど。

*2:出自的にStepstoneのコンパイラの仕様を正統とするべきかもしれないけど、少なくともAppleのものには取り入れられていないと思う。

*3:内部的には使われていたかもしれないけど