Visual Studio 2010 における Code Contracts の設定
Dino Esposito
先月は、Microsoft .NET Framework 4 に実装するソフトウェア コントラクトについて紹介しました。Code Contracts というソフトウェア コントラクトを使用すると、コードが正しく機能するために満たすべき正式な条件を表現できます。メソッドが想定しているデータを受け取らない場合や、想定している事後条件に従わないデータを生成した場合は、Code Contracts によってコードから例外をスローします。事前条件と事後条件の概要については、先月のコラム (msdn.microsoft.com/magazine/gg983479) を参照してください。
Code Contracts は .NET Framework 4 の機能ですが、ランタイム ツール、MSBuild との統合、プロジェクトのプロパティ ダイアログ ボックスのプロパティ ページなど、Visual Studio 2010 のいくつかの機能も使用しています。事前条件と事後条件を記述するだけでは不十分です。ソフトウェア コントラクトを活用する場合、プロジェクトごとにランタイム チェック機能を有効にすることも必要です。この機能を有効にするには、Visual Studio 2010 の Code Contracts プロジェクトのプロパティ ページを使用します。
今月のコラムでは、選択できるさまざまなオプションの目的を説明し、リライター ツールや、Code Contracts を使用して実行する最も一般的な処理である、引数の検証の手法について深く掘り下げます。
Code Contracts のプロパティ ページ
Code Contracts の事前条件と事後条件は、すべてのビルドに設定すべきでしょうか。それとも、デバッグ ビルドのみに設定すべきでしょうか。この点については、ソフトウェア コントラクトをどのように捉えるかによって決まります。ソフトウェア コントラクトは設計作業の一環として行われるのでしょうか。それとも、単なるデバッグ手法でしょうか。
ソフトウェア コントラクトを設計作業の一環と捉えるのであれば、リリース ビルドから Code Contracts を削除する必要はありません。単なるデバッグ手法と捉えるのであれば、リリース モードでコンパイルするときには使用しないことになるでしょう。
.NET Framework では、Code Contracts はフレームワークの一部に過ぎず、あらゆる言語に対応しているわけではありません。結果的には、プロジェクト内でビルドごとに構成する方が簡単です。そのため、.NET Framework でソフトウェア コントラクトを実装する場合、Code Contracts を実装するのに適した場所とタイミングの決定は開発者に委ねられます。
図 1 は、Visual Studio 2010 のプロパティ ページです。このページを使用して、アプリケーションにとってソフトウェア コントラクトがどのように機能するかを設定します。これらの設定は、プロジェクト単位に適用されるため、必要に応じて設定できます。
図 1 Visual Studio 2010 における Code Contracts のプロパティ ページ
対象とする構成 (デバッグ ビルド、リリース ビルドなど) を選択し、設定をその構成にのみ適用できます。これで、Code Contracts をデバッグ ビルドで有効にし、リリース ビルドでは有効にしないようにすることができます。さらに重要なことは、この設定はいつでも切り替えることができます。
ランタイム チェック
Code Contracts を有効にするには、[Perform Runtime Contract Checking] チェック ボックスをオンにする必要があります。このチェック ボックスをオフのままにすると、ソース コードで使用されるコントラクトの指示の効果はなくなります (ただし、DEBUG シンボルを定義したビルドでの Contract.Assert メソッドと Contract.Assume メソッドは例外ですが、気にするほどのことではありません)。このチェック ボックスは、コンパイルの各段階の最後にリライター ツールを実行するかどうかを制御します。リライター ツールは、ソフトウェア コントラクトの後処理を行い、適切な場所に事前条件、事後条件、およびインバリアントのチェックを配置して MSIL コードを変更する外部ツールです。
ただし、次のような事前条件を使用する場合は、リライターを利用しないと、ランタイム アサート エラーが発生することになります。
Contract.Requires<TException>(condition)
図 2 は、このときに表示されるメッセージ ボックスです。
図 2 [Perform Runtime Contract Checking] チェック ボックスをオンにする必要があることを示すコード
ランタイム チェックの動作を詳しく確認するには、次のコードを実行します。
public Int32 Sum(Int32 x, Int32 y) {
// Check input values
ValidateOperands(x, y);
ValidateResult();
// Perform the operation
if (x == y)
return 2 * x;
return x + y;
}
コントラクトの詳細は、先月のコラムで説明したように、ContractAbbreviator を使用して ValidateXxx メソッドに格納します。ValidateXxx メソッドのソース コードを次に示します。
[ContractAbbreviator]
private void ValidateOperands(Int32 x, Int32 y) {
Contract.Requires(x >= 0 && y >= 0);
}
[ContractAbbreviator]
private void ValidateResult() {
Contract.Ensures(Contract.Result<Int32>() >= 0);
}
Contract.Requires<TException> メソッドではなく Contract.Requires メソッドを使用すれば、リライターをビルドの 1 つで利用しない場合でも、図 2 のエラーを回避できます。図 2 のメッセージ ボックスは、次のような Contract.Requires メソッドの内部実装が原因で表示されます。
[Conditional("CONTRACTS_FULL")]
public static void Requires(bool condition, string userMessage) {
AssertMustUseRewriter(
ContractFailureKind.Precondition, "Requires");
}
public static void Requires<TException>(bool condition)
where TException: Exception {
AssertMustUseRewriter(
ContractFailureKind.Precondition, "Requires<TException>");
}
Conditional 属性は、CONTRACTS_FULL シンボルが定義されていなければ、このようなメソッド呼び出しは無視すべきであることをコンパイラに指示しています。このシンボルは、[Perform Runtime Contract Checking] チェック ボックスをオンにした場合にのみ定義されます。Contract.Requires<TException> メソッドは、条件付きで定義されておらず、属性を指定していないため、リライターによるチェックが実行され、ランタイム コントラクト チェックが無効であれば、エラーのアサーションが行われることになります。
では、前のコードでリライターを使用している場合の影響について考えてみましょう。ここでの目的は、ブレークポイントを使用し、Ctrl キーを押しながら F11 キーを押して、Visual Studio 2010 の逆アセンブル ビューを表示することで簡単に確認できます。図 3 は、ランタイム コントラクト チェックを有効にしないでコンパイルした Sum メソッドをステップ実行したときの逆アセンブル ビューの内容です。ご覧のとおり、ソース コードは、クラスで作成したものと同じです。
図 3 ランタイム コントラクト チェックを有効にしなかった場合の逆アセンブル ビュー
ランタイム チェックを有効にすると、リライター ツールはコンパイラの結果を無視して、MSIL コードを編集します。Code Contracts を有効にして同じコードを確認すると、図 4 のようになります。
図 4 return ステートメントの後にチェックされる事後条件
注目すべき違いは、ValidateResult メソッドを、Sum メソッドの終了直前かつ return ステートメントの後に呼び出している点です。図 4 のコードで何が行われているかを理解するために、MSIL の専門家になる必要はありません。メソッドを開始する前に、最上位の事前条件に従ってオペランドを検証します。事後条件を含むコードはメソッドの最下部に移動し、最後の return ステートメントの MSIL コードの直後に置かれます。興味深いことに、1 つ目の return ステートメント (Sum メソッドの抜け道として実装しているステートメント) は、ValidateResult メソッドの開始アドレスにジャンプするようになります。
...
return 2 * x;
00000054 mov eax,dword ptr [ebp-8]
00000057 add eax,eax
00000059 mov dword ptr [ebp-0Ch],eax
0000005c nop
0000005d jmp 0000006B
...
ValidateResult();
0000006b push dword ptr ds:[02C32098h]
...
もう一度、図 1 を見てみましょう。[Perform Runtime Contract Checking] チェック ボックスの横にドロップダウン リストが配置されています。このドロップダウン リストでは、有効にするソフトウェア コントラクトのレベルを設定します。リストには [Full]、[Pre and Post]、[Preconditions]、[ReleaseRequires]、[None] など、さまざまなレベルがあります。
[Full] はすべての種類のソフトウェア コントラクトをサポートすることを示し、[None] はソフトウェア コントラクトを使用しないことを示します。[Pre and Post] では、インバリアントを除外します。[Preconditions] では、Ensures ステートメントも除外します。
[ReleaseRequires] では、Contract.Requires メソッドをサポートしません。Requires<TException> メソッドまたは従来の If-Then-Throw 形式を使用して、事前条件を指定できるだけです。
プロジェクトのプロパティ ページでは、プロジェクト単位にランタイム チェックを有効または無効にできます。では、コードの一部のセクションのみでランタイム チェックを無効にする場合はどうすればよいでしょう。このような場合は、ContractRuntimeIgnored 属性を使用して、プログラムの中でランタイム チェックを無効にすることができます。ただし、最新リリース (1.4.40307.0) では、Contract.ForAll メソッドや Contract.Exists メソッドへの参照を含むコントラクトを実行できないようにする、新しい [Skip Quantifiers] オプションが追加されました。
この属性は、Contract 式で使用するメンバーに適用できます。この属性でメンバーを修飾すると、そのメンバーが存在する Contract ステートメント全体に対して、ランタイム チェックが実行されません。ただし、この属性は、Assert メソッド、Assume メソッドなどの Contract メソッドでは認識されません。
Assembly Mode
Code Contracts のプロパティでは、Code Contracts の Assembly Mode 設定も構成できます。この設定では、引数検証の実行方法を指定します。設定には、[Standard Contract Requires] と [Contract Reference Assembly] の 2 つのオプションがあります。Assembly Mode の設定は、リライターのようなツールがコードの微調整を行い、必要に応じて適切な警告を発生するのに役立ちます。では、Assembly Mode を使用し、パラメーターの検証を目的として、必要な Code Contracts の使用を宣言してみましょう。Assembly Mode 設定では、いくつか簡単な規則を満たす必要があります。そうでなければ、コンパイル エラーが発生することになります。
Requires メソッドと Requires<T> メソッドを使用してメソッド引数を検証する場合は、Assembly Mode を [Standard Contract Requires] に設定します。If-Then-Throw ステートメントを事前条件として使用する場合は、[Custom Parameter Validation] に設定します。[Custom Parameter Validation] に設定しないと、If-Then-Throw ステートメントは Requires<T> メソッドのように扱われます。[Custom Parameter Validation] と Requires ステートメントのいずれかの形式を明示的に組み合わせて使用すると、コンパイル エラーがスローされます。
Requires ステートメントを使用する場合と、If-Then-Throw ステートメントを使用する場合にはどのような違いがあるのでしょう。If-Then-Throw ステートメントは、検証エラーが発生すると、指定した例外を常にスローします。この点においては、Requires ステートメントではなく、Requires<T> メソッドに似ています。また、プレーンな If-Then-Throw ステートメントに EndContractBlock メソッドの呼び出しを続けなければ、コントラクト ツール (リライターや静的チェッカー) ではこのステートメントを検出できません。EndContractBlock メソッドを使用するときは、このメソッドをメソッド内で呼び出す最後の Code Contracts メソッドにする必要があります。このメソッドの後に、他の Code Contracts 呼び出しを続けることはできません。
if (y == 0)
throw new ArgumentException();
Contract.EndContractBlock();
また、Requires ステートメントは自動的に継承されます。If-Then-Throw ステートメントは、EndContractBlock メソッドを使用しない限り継承されません。レガシ モードでは、If-Then-Throw コントラクトは継承されません。代わりに、コントラクトの継承を手動で行う必要があります。ツールは、オーバーライドおよびインターフェイスの実装で、事前条件が繰り返されていることを検出しなければ、警告を試みます。
最後に、ContractAbbreviators には If-Then-Throw ステートメントを含めることができない点に注意してください。ただし、代わりに Contractvalidators を使用できます。ContractAbbreviators には、引数の検証を目的として、正規のコントラクト ステートメントのみ含めることができます。
その他の設定
Code Contracts のプロパティ ページで [Perform Runtime Contract Checking] チェック ボックスをオンにすると、便利な追加オプションが有効になります。
[Assert on Contract Failure] チェック ボックスをオンにしている場合にコントラクトに違反すると、エラーの詳細を示すアサートが発生します。図 2 に示したようなメッセージ ボックスが表示され、いくつかオプションを選択できます。たとえば、デバッガーの再アタッチ、アプリケーションのアボート、単純にエラーを無視して続行などを試みることができます。
このオプションは、デバッグ ビルドのみで使用します。ここで表示される情報は、平均的なエンド ユーザーにとってあまり重要でない可能性があるためです。Code Contracts API には、あらゆるコントラクト違反をキャプチャする、一元管理される例外ハンドラーが用意されています。ただし、何がエラーの原因かを判断するのは開発者です。開発者は受け取った情報で、事前条件、事後条件、またはインバリアントのいずれでエラーが発生したかを判断しますが、エラーを識別する方法は、ブール式を使用するか、構成済みのエラー メッセージを使用することしかありません。つまり、一元管理される例外ハンドラーから適切に回復するのはやや困難です。
Contract.ContractFailed += CentralizedErrorHandler;
ハンドラーを示すコードは、次のとおりです。
static void CentralizedErrorHandler(
Object sender, ContractFailedEventArgs e) {
Console.WriteLine("{0}: {1}; {2}", e.
FailureKind, e.Condition, e.Message);
e.SetHandled();
}
実行時に特定の例外をスローするのであれば、Requires<TException> メソッドを使用するのが適切です。Code Contracts の使用をデバッグ ビルドに限定する場合や、実際の例外の種類を気にしない場合は、Requires メソッドと一元管理されるハンドラーを使用します。多くの場合は、エラーが発生することを示すだけで十分です。たとえば、多くのアプリケーションは、最上位レベルですべての種類の例外をキャッチし再開方法を判断する、キャッチオール手法を使用しています。
[Only Public Surface Contracts] オプションは、Code Contracts を適用する場所 (すべてのメソッドまたはパブリック メソッドのみ) を指定します。このチェック ボックスをオンにすると、リライターはプライベート メンバーと保護されたメンバーの Code Contracts ステートメントは無視し、パブリック メンバーのみに Code Contracts を適用します。
このチェック ボックスをオンにすることは、Code Contracts を設計全体の一部として組み込み、結果として、あらゆる場所で使用する場合に合理的です。ただし、アプリケーションの出荷準備が整ったら、外部コードから内部メンバーを直接呼び出すことはないため、最適化の一環として、内部メンバーのパラメーター チェックという余分な処理を無効にすることができます。
Code Contracts をアセンブリのパブリック サーフェイスに限定するのが適切かどうかは、コードの作成方法によって異なります。パブリック サーフェイスから行われる下位レベルの呼び出しが正しいことを保証できれば、これは一種の最適化です。これを保証できないのに内部メソッドのコントラクトを無効にすると、ソースの厄介なバグにつながる可能性があります。
[Call-site Requires Checking] オプションには、別の最適化のシナリオが用意されています。他のアセンブリ内のモジュールで使用されるライブラリを作成しているとします。パフォーマンス上の理由から、Code Contracts のランタイム チェックを無効にします。しかし、コントラクト参照アセンブリを作成している限り、呼び出し側が呼び出し先の各メソッドのコントラクトをチェックできるようにします。
コントラクト参照アセンブリは、Code Contracts を使用するクラスを含むすべてのアセンブリに存在する可能性があります。このアセンブリは、Code Contracts ステートメントを含む (他のコードは含まない) 親アセンブリのパブリックに参照可能なインターフェイスを含みます。このアセンブリの作成については、Code Contracts プロパティ ページから指示および制御できます。
ライブラリを呼び出すコードでは、コントラクト参照アセンブリを参照する可能性があり、[Call-site Requires Checking] チェック ボックスをオンにするだけで、Code Contracts を自動的にインポートできます。呼び出し側のコードを処理する際、リライターは、コントラクト参照アセンブリを含む外部アセンブリから呼び出されるすべてのメソッドのコントラクトをインポートします。この場合、コントラクトは Call-site (呼び出し側) でチェックされます。そのため、直接制御しないコードに対して、チェックを有効および無効にできるオプションの動作が可能です。
まとめ
Code Contracts は、十分に調査する価値のある .NET Framework の領域です。ここでは、構成オプションの表面をなぞったにすぎず、静的チェッカーの使用については詳しく説明しませんでした。Code Contracts は、アプリケーションをより明確に設計し、わかりやすいコードを作成するのに役立ちます。
Code Contracts の詳細については、2009 年 8 月号の Melitta Andersen による「CLR 徹底解剖」コラム (msdn.microsoft.com/magazine/ee236408) と、DevLabs の Code Contracts のサイト (msdn.microsoft.com/devlabs/dd491992、英語) を参照してください。また、Code Contracts の開発に関する興味深い背景情報を、Microsoft Research の Code Contracts のサイト (research.microsoft.com/projects/contracts、英語) でご覧いただけます。
Dino Esposito は、『Programming Microsoft ASP.NET MVC』(Microsoft Press、2010 年) の著者であり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者です。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。Twitter (twitter.com/despos、英語) で彼をフォローしてください。
この記事のレビューに協力してくれた技術スタッフの Mike Barnett に心より感謝いたします。