注
この記事は .NET Framework に固有のものです。 .NET 6 以降のバージョンを含む、.NET の新しい実装には適用されません。
コード コントラクトは、.NET Framework コードで前提条件、事後条件、およびオブジェクト不変条件を指定する方法を提供します。 前提条件は、メソッドまたはプロパティを入力するときに満たす必要がある要件です。 事後条件は、メソッドまたはプロパティ コードが終了した時点での期待値を表します。 オブジェクトインバリアントは、適切な状態にあるクラスの予期される状態を表します。
注
.NET 5 以降 (.NET Core バージョンを含む) では、コード コントラクトはサポートされていません。 代わりに Null 許容参照型 を使用することを検討してください。
コード コントラクトには、コードをマークするためのクラス、コンパイル時分析用の静的アナライザー、ランタイム アナライザーが含まれます。 コード コントラクトのクラスは、 System.Diagnostics.Contracts 名前空間にあります。
コード コントラクトの利点は次のとおりです。
テストの改善: コード コントラクトは、静的コントラクトの検証、ランタイム チェック、およびドキュメントの生成を提供します。
自動テスト ツール: 前提条件を満たさない無意味なテスト引数を除外することで、コード コントラクトを使用して、より意味のある単体テストを生成できます。
静的検証: 静的チェッカーは、プログラムを実行せずにコントラクト違反があるかどうかを判断できます。 null 逆参照や配列の境界、明示的コントラクトなど、暗黙的なコントラクトがチェックされます。
リファレンス ドキュメント: ドキュメント ジェネレーターは、既存の XML ドキュメント ファイルをコントラクト情報で拡張します。 Sandcastle で使用できるスタイル シートもあり、生成されたドキュメント ページにコントラクト セクションが含まれます。
すべての .NET Framework 言語は、コントラクトをすぐに利用できます。特別なパーサーやコンパイラを記述する必要はありません。 Visual Studio アドインを使用すると、実行するコード コントラクト分析のレベルを指定できます。 アナライザーは、コントラクトが整形式 (型チェックと名前解決) であることを確認し、共通中間言語 (CIL) 形式のコントラクトのコンパイル済み形式を生成できます。 Visual Studio でコントラクトを作成すると、ツールによって提供される標準の IntelliSense を利用できます。
コントラクト クラスのほとんどのメソッドは条件付きでコンパイルされます。つまり、コンパイラは、 #define
ディレクティブを使用して特別なシンボル (CONTRACTS_FULL) を定義した場合にのみ、これらのメソッドの呼び出しを出力します。 CONTRACTS_FULLでは、 #ifdef
ディレクティブを使用せずにコード内にコントラクトを記述できます。異なるビルド、コントラクトを含むビルド、使用しないビルドを生成できます。
コード コントラクトを使用するためのツールと詳細な手順については、Visual Studio マーケットプレース サイトの コード コントラクト を参照してください。
前提 条件
Contract.Requiresメソッドを使用して、前提条件を表すことができます。 前提条件は、メソッドが呼び出されたときの状態を指定します。 一般に、有効なパラメーター値を指定するために使用されます。 前提条件で言及されているすべてのメンバーは、少なくともメソッド自体と同じくらいアクセス可能である必要があります。そうしないと、メソッドのすべての呼び出し元が前提条件を理解できない可能性があります。 条件には副作用を含めてはなりません。 失敗した前提条件の実行時動作は、ランタイム アナライザーによって決定されます。
たとえば、次の前提条件は、パラメーター x
が null 以外である必要があることを表しています。
Contract.Requires(x != null);
前提条件の失敗時にコードで特定の例外をスローする必要がある場合は、次のように Requires のジェネリック オーバーロードを使用できます。
Contract.Requires<ArgumentNullException>(x != null, "x");
従来のステートメントが必要
ほとんどのコードには、コードの形式でパラメーターの検証 if
-then
-throw
含まれています。 コントラクト ツールは、次の場合にこれらのステートメントを前提条件として認識します。
ステートメントは、メソッド内の他のステートメントの前に表示されます。
このようなステートメントのセット全体の後に、Requires、Ensures、EnsuresOnThrow、EndContractBlock メソッドの呼び出しなど、明示的なContract メソッド呼び出しが続きます。
if
-
then
-
throw
ステートメントがこの形式で表示されると、ツールはそれらを従来のrequires
ステートメントとして認識します。 他のコントラクトが if
-then
-throw
シーケンスに従っていない場合は、 Contract.EndContractBlock メソッドでコードを終了します。
if (x == null) throw new ...
Contract.EndContractBlock(); // All previous "if" checks are preconditions
前のテストの条件は否定された前提条件であることに注意してください。 (実際の前提条件は x != null
。否定された前提条件は高度に制限されています。前の例に示すように記述する必要があります。つまり、 else
句は含めず、 then
句の本体は単一の throw
ステートメントである必要があります。
if
テストは、純度と可視性の両方の規則に従いますが (使用ガイドラインを参照)、throw
式は純度規則のみに従います。 ただし、スローされる例外の型は、コントラクトが発生するメソッドと同じように表示される必要があります。
事後条件
事後条件は、メソッドの終了時の状態のコントラクトです。 事後条件は、メソッドを終了する直前にチェックされます。 失敗した事後条件の実行時動作は、ランタイム アナライザーによって決定されます。
前提条件とは異なり、事後条件は、可視性の低いメンバーを参照する場合があります。 クライアントは、プライベート状態を使用して事後条件によって表される情報の一部を理解または利用できない場合がありますが、これはクライアントがメソッドを正しく使用する機能には影響しません。
標準の事後条件
Ensuresメソッドを使用して、標準の事後条件を表すことができます。 事後条件は、メソッドの正常終了時に true
する必要がある条件を表します。
Contract.Ensures(this.F > 0);
例外的な事後条件
例外的な事後条件は、メソッドによって特定の例外がスローされたときに true
する必要がある事後条件です。 次の例に示すように、 Contract.EnsuresOnThrow メソッドを使用して、これらの事後条件を指定できます。
Contract.EnsuresOnThrow<T>(this.F > 0);
引数は、T
のサブタイプである例外がスローされるたびにtrue
する必要がある条件です。
例外の種類には、例外的な事後条件で使用するのが困難なものもあります。 たとえば、T
にException型を使用するには、スローされる例外の種類に関係なく、スタック オーバーフローやその他の制御不可能な例外であっても、メソッドで条件を保証する必要があります。 例外の事後条件は、メンバーが呼び出されたときにスローされる可能性がある特定の例外 (たとえば、TimeZoneInfo メソッド呼び出しに対してInvalidTimeZoneExceptionがスローされた場合など) にのみ使用する必要があります。
特殊な事後条件
次のメソッドは、事後条件内でのみ使用できます。
メソッドの戻り値を事後条件で参照するには、式
Contract.Result<T>()
を使用します。ここで、T
はメソッドの戻り値の型に置き換えられます。 コンパイラが型を推論できない場合は、明示的に指定する必要があります。 たとえば、C# コンパイラは引数を受け取らないメソッドの型を推論できないため、次の事後条件が必要です。戻り値の型がvoid
Contract.Ensures(0 <Contract.Result<int>())
メソッドは、事後条件でContract.Result<T>()
を参照できません。事後条件のプリステート値は、メソッドまたはプロパティの先頭にある式の値を参照します。 式
Contract.OldValue<T>(e)
を使用します。ここで、T
はe
の型です。 コンパイラが型を推論できる場合は常に、ジェネリック型引数を省略できます。 (たとえば、C# コンパイラは、引数を受け取るため、常に型を推論します)。e
で発生する可能性がある内容と、古い式が表示されるコンテキストには、いくつかの制限があります。 古い式に別の古い式を含めることはできません。 最も重要なのは、古い式は、メソッドの前提条件状態に存在する値を参照する必要があることです。 つまり、メソッドの前提条件がtrue
されている限り評価できる式である必要があります。 そのルールのインスタンスをいくつか次に示します。値は、メソッドの前提条件の状態に存在する必要があります。 オブジェクトのフィールドを参照するには、前提条件によってオブジェクトが常に null 以外であることを保証する必要があります。
古い式でメソッドの戻り値を参照することはできません。
Contract.OldValue(Contract.Result<int>() + x) // ERROR
古い式で
out
パラメーターを参照することはできません。量指定子の範囲がメソッドの戻り値に依存している場合、古い式は量指定子のバインドされた変数に依存できません。
Contract.ForAll(0, Contract.Result<int>(), i => Contract.OldValue(xs[i]) > 3); // ERROR
古い式は、メソッド呼び出しのインデクサーまたは引数として使用されない限り、 ForAll または Exists 呼び出しで匿名デリゲートのパラメーターを参照できません。
Contract.ForAll(0, xs.Length, i => Contract.OldValue(xs[i]) > 3); // OK Contract.ForAll(0, xs.Length, i => Contract.OldValue(i) > 3); // ERROR
匿名デリゲートが ForAll または Exists メソッドの引数でない限り、古い式の値が匿名デリゲートのパラメーターのいずれかに依存している場合、古い式は匿名デリゲートの本体で発生できません。
Method(... (T t) => Contract.OldValue(... t ...) ...); // ERROR
Out
メソッドの本体の前にコントラクトが表示され、ほとんどのコンパイラでは事後条件でパラメーターをout
参照できないため、パラメーターに問題があります。 この問題を解決するために、 Contract クラスは ValueAtReturn メソッドを提供します。これにより、out
パラメーターに基づいて事後条件が可能になります。public void OutParam(out int x) { Contract.Ensures(Contract.ValueAtReturn(out x) == 3); x = 3; }
OldValueメソッドと同様に、コンパイラが型を推論できる場合は常にジェネリック型パラメーターを省略できます。 コントラクト リライターは、メソッド呼び出しを
out
パラメーターの値に置き換えます。 ValueAtReturn メソッドは事後条件でのみ使用できます。 メソッドの引数は、out
パラメーターまたは構造体out
パラメーターのフィールドである必要があります。 後者は、構造体コンストラクターの事後条件のフィールドを参照する場合にも便利です。注
現在、コード コントラクト分析ツールでは、
out
パラメーターが正しく初期化されているかどうかを確認せず、事後条件でのメンションを無視しています。 したがって、前の例では、コントラクトの後の行が整数を割り当てる代わりにx
の値を使用していた場合、コンパイラは正しいエラーを発行しません。 ただし、CONTRACTS_FULL プリプロセッサ シンボルが定義されていないビルド (asa リリース ビルドなど) では、コンパイラはエラーを発行します。
不変量
オブジェクトインバリアントは、そのオブジェクトがクライアントに表示されるたびに、クラスの各インスタンスに対して true である必要がある条件です。 オブジェクトが正しいと見なされる条件を表します。
インバリアント メソッドは、 ContractInvariantMethodAttribute 属性でマークされることで識別されます。 インバリアント メソッドには、次の例に示すように、 Invariant メソッドの一連の呼び出しを除き、コードを含めてはなりません。各メソッドは、個々の不変を指定します。
[ContractInvariantMethod]
protected void ObjectInvariant ()
{
Contract.Invariant(this.y >= 0);
Contract.Invariant(this.x > this.y);
...
}
インバリアントは、CONTRACTS_FULL プリプロセッサ シンボルによって条件付きで定義されます。 実行時のチェック中に、インバリアントは各パブリック メソッドの末尾でチェックされます。 インバリアントが同じクラスのパブリック メソッドに言及した場合、そのパブリック メソッドの最後に通常発生するインバリアント チェックは無効になります。 代わりに、チェックは、そのクラスに対する最も外側のメソッド呼び出しの最後にのみ発生します。 これは、別のクラスのメソッドの呼び出しが原因でクラスが再入力された場合にも発生します。 インバリアントは、オブジェクト ファイナライザーと IDisposable.Dispose 実装ではチェックされません。
使用方法のガイドライン
コントラクトの順序付け
次の表は、メソッド コントラクトを記述するときに使用する必要がある要素の順序を示しています。
If-then-throw statements |
下位互換性のあるパブリック前提条件 |
---|---|
Requires | すべてのパブリック前提条件。 |
Ensures | すべてのパブリック (通常) 事後条件。 |
EnsuresOnThrow | すべてのパブリック例外事後条件。 |
Ensures | すべてのプライベート/内部 (通常) 事後条件。 |
EnsuresOnThrow | すべてのプライベート/内部例外事後条件。 |
EndContractBlock | 他のコントラクトなしで if -then -throw スタイルの前提条件を使用する場合は、 EndContractBlock を呼び出して、チェックが前提条件であるかどうかを示します。 |
純度
コントラクト内で呼び出されるすべてのメソッドは純粋である必要があります。つまり、既存の状態を更新することはできません。 純粋メソッドは、純粋メソッドに入った後に作成されたオブジェクトを変更できます。
コード コントラクト ツールは、現在、次のコード要素が純粋であると想定しています。
PureAttributeでマークされているメソッド。
PureAttributeでマークされている型 (属性はすべての型のメソッドに適用されます)。
プロパティの取得アクセサー。
演算子 (名前が "op" で始まり、1 つまたは 2 つのパラメーターと void 以外の戻り値の型を持つ静的メソッド)。
完全修飾名が "System.Diagnostics.Contracts.Contract"、"System.String"、"System.IO.Path"、または "System.Type" で始まるすべてのメソッド。
デリゲート型自体が PureAttributeに属性付けされている場合は、呼び出されたデリゲート。 デリゲート型 System.Predicate<T> と System.Comparison<T> は純粋と見なされます。
視認性
コントラクトで言及されているすべてのメンバーは、少なくとも、そのメンバーが表示されるメソッドと同じ表示である必要があります。 たとえば、パブリック メソッドの前提条件でプライベート フィールドを指定することはできません。クライアントは、メソッドを呼び出す前に、このようなコントラクトを検証できません。 ただし、フィールドが ContractPublicPropertyNameAttributeでマークされている場合は、これらの規則から除外されます。
例
次の例は、コード コントラクトの使用を示しています。
#define CONTRACTS_FULL
using System;
using System.Diagnostics.Contracts;
// An IArray is an ordered collection of objects.
[ContractClass(typeof(IArrayContract))]
public interface IArray
{
// The Item property provides methods to read and edit entries in the array.
Object this[int index]
{
get;
set;
}
int Count
{
get;
}
// Adds an item to the list.
// The return value is the position the new element was inserted in.
int Add(Object value);
// Removes all items from the list.
void Clear();
// Inserts value into the array at position index.
// index must be non-negative and less than or equal to the
// number of elements in the array. If index equals the number
// of items in the array, then value is appended to the end.
void Insert(int index, Object value);
// Removes the item at position index.
void RemoveAt(int index);
}
[ContractClassFor(typeof(IArray))]
internal abstract class IArrayContract : IArray
{
int IArray.Add(Object value)
{
// Returns the index in which an item was inserted.
Contract.Ensures(Contract.Result<int>() >= -1);
Contract.Ensures(Contract.Result<int>() < ((IArray)this).Count);
return default(int);
}
Object IArray.this[int index]
{
get
{
Contract.Requires(index >= 0);
Contract.Requires(index < ((IArray)this).Count);
return default(int);
}
set
{
Contract.Requires(index >= 0);
Contract.Requires(index < ((IArray)this).Count);
}
}
public int Count
{
get
{
Contract.Requires(Count >= 0);
Contract.Requires(Count <= ((IArray)this).Count);
return default(int);
}
}
void IArray.Clear()
{
Contract.Ensures(((IArray)this).Count == 0);
}
void IArray.Insert(int index, Object value)
{
Contract.Requires(index >= 0);
Contract.Requires(index <= ((IArray)this).Count); // For inserting immediately after the end.
Contract.Ensures(((IArray)this).Count == Contract.OldValue(((IArray)this).Count) + 1);
}
void IArray.RemoveAt(int index)
{
Contract.Requires(index >= 0);
Contract.Requires(index < ((IArray)this).Count);
Contract.Ensures(((IArray)this).Count == Contract.OldValue(((IArray)this).Count) - 1);
}
}
#Const CONTRACTS_FULL = True
Imports System.Diagnostics.Contracts
' An IArray is an ordered collection of objects.
<ContractClass(GetType(IArrayContract))> _
Public Interface IArray
' The Item property provides methods to read and edit entries in the array.
Default Property Item(ByVal index As Integer) As [Object]
ReadOnly Property Count() As Integer
' Adds an item to the list.
' The return value is the position the new element was inserted in.
Function Add(ByVal value As Object) As Integer
' Removes all items from the list.
Sub Clear()
' Inserts value into the array at position index.
' index must be non-negative and less than or equal to the
' number of elements in the array. If index equals the number
' of items in the array, then value is appended to the end.
Sub Insert(ByVal index As Integer, ByVal value As [Object])
' Removes the item at position index.
Sub RemoveAt(ByVal index As Integer)
End Interface 'IArray
<ContractClassFor(GetType(IArray))> _
Friend MustInherit Class IArrayContract
Implements IArray
Function Add(ByVal value As Object) As Integer Implements IArray.Add
' Returns the index in which an item was inserted.
Contract.Ensures(Contract.Result(Of Integer)() >= -1) '
Contract.Ensures(Contract.Result(Of Integer)() < CType(Me, IArray).Count) '
Return 0
End Function 'IArray.Add
Default Property Item(ByVal index As Integer) As Object Implements IArray.Item
Get
Contract.Requires(index >= 0)
Contract.Requires(index < CType(Me, IArray).Count)
Return 0 '
End Get
Set(ByVal value As [Object])
Contract.Requires(index >= 0)
Contract.Requires(index < CType(Me, IArray).Count)
End Set
End Property
Public ReadOnly Property Count() As Integer Implements IArray.Count
Get
Contract.Requires(Count >= 0)
Contract.Requires(Count <= CType(Me, IArray).Count)
Return 0 '
End Get
End Property
Sub Clear() Implements IArray.Clear
Contract.Ensures(CType(Me, IArray).Count = 0)
End Sub
Sub Insert(ByVal index As Integer, ByVal value As [Object]) Implements IArray.Insert
Contract.Requires(index >= 0)
Contract.Requires(index <= CType(Me, IArray).Count) ' For inserting immediately after the end.
Contract.Ensures(CType(Me, IArray).Count = Contract.OldValue(CType(Me, IArray).Count) + 1)
End Sub
Sub RemoveAt(ByVal index As Integer) Implements IArray.RemoveAt
Contract.Requires(index >= 0)
Contract.Requires(index < CType(Me, IArray).Count)
Contract.Ensures(CType(Me, IArray).Count = Contract.OldValue(CType(Me, IArray).Count) - 1)
End Sub
End Class
.NET