.NETプラットフォームでのコントラクトプログラミングの機能を親切に提供する
Code Contractsライブラリは、
Contractクラスの静的メソッドを使用して前提条件と事後条件を設定します。 一方では、属性に基づいた代替実装があまりにも制限されるため、これは良いことです。 一方、これは、本質的にコードを含まないインターフェイスコントラクトまたは抽象メソッドに関して、特定の困難を追加します。つまり、メソッドを呼び出す方法はありません。
これは、インターフェースまたは抽象クラスにハングアップする
ContractClassAttributeと、コントラクト自体にハングアップする
ContractClassForAttributeの 2つの属性を使用して解決されます。
/// <summary> /// Custom collection interface /// </summary> [ContractClass(typeof(CollectionContract))] public interface ICollection { void Add(string s); int Count { get; } bool Contains(string s); } /// <summary> /// Contract class for <see cref="ICollection"/>. /// </summary> [ContractClassFor(typeof(ICollection))] internal abstract class CollectionContract : ICollection { public void Add(string s) { Contract.Ensures(Count >= Contract.OldValue(Count)); Contract.Ensures(Contains(s)); } public int Count { get { Contract.Ensures(Contract.Result<int>() >= 0); return default(int); } } [Pure] public bool Contains(string s) { return default(bool); } }
この
ICollectionインターフェースの有用性は疑わしいように見えますが、それらの助けにより、インターフェースの継承に関連する契約のすべての必要な機能と制限を見ることができます。 この例の焦点は、
CollectionContractクラスの2つのメンバー、
Addメソッドと
Countプロパティです。これらのメソッドは、対応するメソッドの前提条件/事後条件を指定します。
現在、一部のクラスが
ICollectionインターフェイスを実装し、事後条件に違反している場合、実行時に例外(特定のCONTRACT_FULL文字を含む)として、また場合によってはStatic Checkerによる静的コード分析中にこれが表示されます。
internal class CustomCollection : ICollection { private readonly List<string> _backingList = new List<string>(); public void Add(string s) {
この場合、これはまさに起こることです。静的チェッカーは、
Addメソッドの事後条件が満たされない場合があると判断します(既存の要素を追加するとき、itJを削除します)。 しかし、彼を信じていない場合、実行中に契約違反が発生する可能性があります。
[Test] public void TestAddTwiceAddsTwoElements() { var collection = new CustomCollection(); int oldCount = collection.Count; collection.Add(""); collection.Add(""); Assert.That(collection.Count, Is.EqualTo(oldCount + 2)); }
Assertが呼び出されると、このテストは失敗します
。 それ以前は、次の例外
を使用して、
Addメソッドを再度呼び出そうとすると、
System.Diagnostics.Contracts .__ ContractsRuntime + ContractException:Postcondition failed:Count> = Contract.OldValue(Count)注
静的分析は「契約プログラミング」の最も興味深い機能の1つですが、コードコントラクトライブラリのこのことは実際のプロジェクトにはまだ準備ができていないと安全に言えます。 第一に、コンパイル時間は桁違いに長くなる可能性があります(!)、そして第二に、その周りで何が起こっているのかを理解するために、タンバリンで子供っぽくジャンプする必要がありますが、この場合でも困難なケースではほとんど何もできません。 CustomCollectionクラスのような単純な例でさえ、静的プロパティアナライザーが何が起こっているのか理解できず、大量の警告を出すため、 Countプロパティに事後条件を手動で追加する必要がありました。 宣言、文書化、関係の形式化など、契約の他のすべての利点 残りますが、コンパイル時にではなく、実行時に(たとえば、単体テストと連動して)機能します。前提条件の緩和と事後条件の強化
コントラクトにより、クラスとそのクライアント間だけでなく、クラスとその子孫間の関係も形式化できます。 仮想メソッドの前提条件は、このメソッドを呼び出すために何を行う必要があるかをクライアントに伝え、後条件はこのメソッドが何をするかを伝えます。 さらに、クライアントコードは、それが動作するオブジェクトの動的タイプが何であるかに関係なく、このコントラクトの履行を信頼できます。 これはまさに、
前回お話ししたリスコフ代入原理が語っていることです。
ただし、代用の原則は、クライアントの仮定を「破らない」場合、相続人がメソッドのセマンティクスを変更することを禁止しません。 そのため、継承者でオーバーライドされたメソッドの前提条件は、呼び出し元コードでそれほど厳密ではない可能性があり(より弱い前提条件を含む場合があります)、事後条件はより厳密である可能性があります。 ロシア語からロシア語に翻訳するために、簡単な例を見てみましょう。
class Base { public virtual object Foo(string s) { Contract.Requires(!string.IsNullOrEmpty(s)); Contract.Ensures(Contract.Result<object>() != null); return new object(); } } class Derived : Base { public override object Foo(string s) {
この例では、相続人メソッドの必要量が少なくなりました。空の文字列が正しい値になりました。 より正確な結果が得られ
ます。オブジェクトが返されるだけでなく、
文字列型
が返され
ます (ただし、コンパイラではなく静的チェッカーによって保証されます)。
注
事後条件強化の典型的な例は、派生クラスがより具体的な型を返す機能です。 この機能は戻り値型共分散と呼ばれ、C ++やJavaなどの言語で使用できます。 C#がこの機能をサポートしている場合、Derived.Fooメソッドの署名を変更して、 オブジェクトではなく文字列を返すことができます 。 前提条件を弱め、後条件を強化するもう1つの例は、C#言語の4番目のバージョンから利用可能なデリゲートとインターフェイスの共分散と反分散です。 契約による設計 の記事で、条件の「厳格さ」について詳しく読んでください 。 ソフトウェアの正確性 、および契約と継承について- 契約による設計 の記事 。 継承Code Contracts開発者は、事後条件を無意味に弱める可能性があると考えたため、そのような機会はありません。 上記の
Derivedクラスコードはコンパイルされますが、前提条件は
Derived method
です。 Fooは弱まりません。つまり、空の文字列を渡すと、前提条件に違反します。 ただし、事前条件とは異なり、事後条件では
ほぼすべてのものが順番に並んでいます。 事後条件(ところで、クラスの不変条件のような)は「合計」されます。これにより、より多くのことを保証できます。 (
Derived。Fooメソッドの本体を変更して、場合によっては
文字列ではなく
intを返す場合、この違反は静的チェッカーによって検出され、実行時にチェックされます。)
事後条件とインターフェース
それでは、基本クラスからインターフェースに移りましょう。 最初のセクションでは、コレクションの要素数を「削減しない」事後条件である
ICollectionインターフェイスを調べました。 この事後条件は、BCLの
TCollectionのICollection のコントラクトに基づいて取得されます。 コードコントラクトをインストールした後、独自のクラスのコントラクトを作成できるだけでなく、BCLの標準クラスコントラクトを使用することもできます。
しかし、標準のインターフェイスコントラクトの分析に進む前に、独自のインターフェイスの階層を作成して、
Addメソッドの事後条件を試してみましょう。
[ContractClass(typeof(ListContract))] public interface IList : ICollection { } [ContractClassFor(typeof(IList))] internal abstract class ListContract : IList { public void Add(string s) {
実際の
IListインターフェースは、事後条件だけでなく、他の多くの興味深いものも追加し
ますが、この場合は重要ではありません。 次に、インターフェイスの事後条件に違反する
IListインターフェイスを実装するクラスを追加します。
public class CustomList : IList { private readonly List<string> _backingList = new List<string>(); public void Add(string s) {
IListインターフェースの
Addメソッドの事後条件に明らかに違反し
ます。要素の数が1つではなく、すぐに2増えるためです。しかし、悲しいことは、静的アナライザーもリライタもインターフェースの事後条件の強化にまったく反応しないことです。 実際、この機能はコードコントラクトライブラリではサポートされていません(さらに、開発者はこれをバグではなく機能と見なしています。詳細は
こちら )。 そのため、現時点では、仮想メソッドの事後条件を強化できます。何らかのインターフェイスを実装するクラスで
は事後条件を強化できますが、相続人のインターフェイスでは事後条件を強化できません !
この点の不愉快さは次のとおりです:まず、インターフェースのより厳密な事後条件の存在を見つける唯一の方法は、契約コードを手動で検索することです(静的チェッカーもリライターも相続事後条件に関する情報を結果コードに追加しないことを思い出します); 第二に、この例は人為的ではなく、標準のBCLコレクションインターフェイスを使用しているときにこの問題が発生する可能性があります。
TのICollectionとTの IListの 契約
mscorlibのアセンブリを注意深く掘り下げた場合
。 契約 コードコントラクトをインストールした後に表示される
dllを使用すると、特に.NET Frameworkの標準クラスのコントラクトとコレクションのコントラクトについて多くの興味深いことがわかります。
Tのメイン
ICollection および Tの IListインターフェイスメソッドのコントラクトは次のとおり
です 。
// From mscorlib.Contracts.dll [ContractClassFor(typeof(ICollection<>))] internal abstract class ICollectionContract<T> : ICollection<T>, IEnumerable<T>, IEnumerable { public void Add([MarshalAs(UnmanagedType.Error)] T item) { Contract.Ensures(this.Count >= Contract.OldValue<int>(this.Count), "this.Count >= Contract.OldValue(this.Count)"); } public void Clear() { Contract.Ensures(this.Count == 0, "this.Count == 0"); } public int Count { get { int num = 0; Contract.Ensures(Contract.Result<int>() >= 0, "Contract.Result<int>() >= 0"); return num; } } } // From mscorlib.Contracts.dll [ContractClassFor(typeof (IList<>))] internal abstract class IListContract<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { void ICollection<T>.Add([MarshalAs(UnmanagedType.Error)] T item) { Contract.Ensures(this.Count == (Contract.OldValue<int>(this.Count) + 1), "Count == Contract.OldValue(Count) + 1"); } public int Count { get { int num = 0; return num; } } }
注
.NET Frameworkのさまざまなバージョンの契約はさまざまな場所にあります。たとえば、フレームワークの4番目のバージョンの契約は、次のパスにあります。「%PROGRAMS%\ Microsoft \ Contracts \ Contracts \ .NETFramework \ v.4.0 \」。 契約のあるアセンブリには、OriginalAssemblyName.Contracts.dll:mscorlib.Contracts.dll、System.Contracts.dll、System.Xml.Contracts.dllという名前が付けられます。ご覧のとおり、リストの事後条件は非常に強力であり、
Addメソッドを呼び出すと、リストに新しい要素が1つだけ表示される必要があります。 2つのインターフェイスの事後条件の違いは、
Addメソッドを呼び出すときにすべてのBCLコレクションが新しい要素を追加するわけではないという事実によるものです(
HashSetと
SortedSetが既にコレクションにある場合、要素を追加しません)。 ただし、すべてのリストに追加される新しいアイテムは1つだけです。 この問題は、明示的な事後条件を特定のコレクションクラス(
Tの リスト 、またはこの例では
DoubleListクラス)に追加することで解決されますが、この場合、インターフェイスコントラクトの主な機能は失われます:クラスファミリの動作を指定する機能。
おわりに
.NETでは期待される動作に関する情報が含まれていないため、すべての開発者がインターフェイスコントラクトまたは抽象メソッドについて快適に考えるわけではありません。 しかし、一方でそれを見ると、そのような方法の契約の重要性ははるかに高くなっています。 特定のメソッドの場合、その実装を見て、明示的または暗黙的な前提条件と事後条件を決定できます。 しかし、インターフェイスの契約を決定するには、不十分な非公式の文書から進むか、このインターフェイスのすべての実装を分析して、置換原則に従って違反されるべきではないその動作の「共通」分母を決定します。
サイトリンク
- GitHubのContractsAndInheritanceプロジェクト 。 この記事のすべての例とテストおよびコメントが含まれています。
- バートランド・マイヤー。 ソフトウェアシステムのオブジェクト指向設計
- 契約による設計。 継承