プログラムをどう完璧に作り上げても防止できないエラーは例外と呼び、バグが無ければ発生しないエラーとは区別します。前者は防ぎようが無いので、言語の例外機能を使ってエラー判定と発生時の処理を例外ケースとして作りますね。後者は要するに発生=バグなので、エラー判定は記載するものの開発版でのみ動作するようにし、完成したプログラムではチェックしないようにします。もっとも手動でエラー処理を追加したり削除したりするのは大変なので、普通は言語のassertion機能を使います。これはよく知られた話だと思いますが、あまり引数チェック時にこの区別を適用する話は聞かないような気がします。つまり、内部でしか呼ばれない関数・メソッドの引数チェックはassertionで良いですし、そうした方が良いケースもあるわけです。
イマイチな例ですが、ある文字列中にある特定のフレーズすべてを一括置換するメソッドMyReplaceAllを次のように実装したとします(コンパイルとかしていないので動かないかも)。
// strに含まれるすべてのtokenをnewTokenに置換する
public void MyReplaceAll( string str, string token, string newToken )
{
if( str == null ) throw new ArgumentNullException( "str" );
if( token == null ) throw new ArgumentNullException( "token" );
if( newToken == null ) throw new ArgumentNullException( "newToken" );
if( token == "" ) throw new ArgumentException( "token" );
// 置換対象のトークンすべてを大文字小文字を区別せず先頭から検索し、置き換える
index = str.IndexOf( token,
0,
StringComparison.CurrentCultureIgnoreCase );
while( 0 <= index ) {
MyReplace( str, index, token.Length, newToken );
index = str.IndexOf( token,
index + newToken.Length,
StringComparison.CurrentCultureIgnoreCase );
}
}
// strのindexからlength文字をnewTokenに置換する
public void MyReplace( string str, int index, int length, string newToken )
{
if( str == null ) throw new ArgumentNullException( "str" );
if( index < 0 || str.Length <= index ) throw new ArgumentOutOfRangeException( "index" );
if( length < 0 || str.Length <= index + length ) throw new ArgumentOutOfRangeException( "length" );
if( newToken == null ) throw new ArgumentNullException( "newToken" );
str.Remove( index, token.Length );
str.Insert( index, newToken );
}
この例では、MyReplaceAllは個々のフレーズを置換するために公開メソッドのMyReplaceを呼び出しています。そしてMyReplaceは公開メソッドですから引数エラーを例外として扱っています。まあ普通ですね。
さて、ここでMyReplaceAllメソッドからMyReplaceを呼び出す部分に注目すると、不正な引数を渡してしまう可能性は無いと言えます。ということは、MyReplaceAllから呼ぶ場合に限って言えばMyReplace内の引数チェックは無駄ですね。そこで、処理を実装した関数をそのまま公開するのではなく「非公開関数として実装した実質の処理を呼び出すだけの関数」を公開する、というテクニックが使えます。この例に適用すると、MyReplaceの実装を切り出した別の下請け関数を作成し、MyReplaceAllもMyReplaceもそれを呼び出す形になります。次のようになるでしょう。
// strに含まれるすべてのtokenをnewTokenに置換する
public void MyReplaceAll( string str, string token, string newToken )
{
if( str == null ) throw new ArgumentNullException( "str" );
if( token == null ) throw new ArgumentNullException( "token" );
if( newToken == null ) throw new ArgumentNullException( "newToken" );
if( token == "" ) throw new ArgumentException( "token" );
// 置換対象のトークンすべてを大文字小文字を区別せず先頭から検索し、置き換える
index = str.IndexOf( token,
0,
StringComparison.CurrentCultureIgnoreCase );
while( 0 <= index ) {
MyReplace_Impl( str, index, token.Length, newToken );
index = str.IndexOf( token,
index + newToken.Length - token.Length,
StringComparison.CurrentCultureIgnoreCase );
}
}
// strのindexからlength文字をnewTokenに置換する
public void MyReplace( string str, int index, int length, string newToken )
{
if( str == null ) throw new ArgumentNullException( "str" );
if( index < 0 || str.Length <= index ) throw new ArgumentOutOfRangeException( "index" );
if( length < 0 || str.Length <= index + length ) throw new ArgumentOutOfRangeException( "length" );
if( newToken == null ) throw new ArgumentNullException( "newToken" );
MyReplace_Impl( str, index, length, newToken );
}
private void MyReplace_Impl( string str, int index, int length, string newToken )
{
Debug.Assert( str != null );
Debug.Assert( 0 <= index && index < str.Length );
Debug.Assert( 0 <= length && index + length < str.Length);
Debug.Assert( newToken != null );
str.Remove( index, token.Length );
str.Insert( index, newToken );
}
MyReplace_Implは非公開メソッドですから、引数チェックはassertionで書けます。このようにすると、MyReplaceAllでMyReplaceを呼び出すよりも無駄が少なくなります。
大半のケースでは安全のため、つまり我々プログラマ自身のミスに対する保険として、下請け関数であったとしても引数のNULLチェックや範囲チェック程度はしておくべきでしょう。引数チェックの省略は危険です。しかし、かといって引数チェックを常に行うのも適切とは言い切れません。たとえばループの中で呼ばれる ループの中で呼ばれる ループの中で呼ばれる…(略)…で呼ばれる関数で普通に引数チェックを実装すると、莫大な回数の引数チェックが発生してパフォーマンス劣化につながる可能性もあります。そこで、それらの間を取って引数をassertionでチェックする、というのは一つ改善策として検討する価値があると言えるでしょう。
引数チェックにassertionを使うのは、多くのケースでは良い習慣ではないかもしれませんが、時として価値があります。