Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Uwaga / Notatka
Ten artykuł jest specyficzny dla programu .NET Framework. Nie ma zastosowania do nowszych implementacji platformy .NET, w tym .NET 6 i nowszych wersji.
Kontrakty kodu umożliwiają określenie warunków wstępnych, pokondycji i niezmiennych obiektów w kodzie programu .NET Framework. Warunki wstępne są wymaganiami, które muszą zostać spełnione podczas wprowadzania metody lub właściwości. Postconditions opisują oczekiwania po zakończeniu metody lub kodu właściwości. Niezmienne obiekty opisują oczekiwany stan klasy, która jest w dobrym stanie.
Uwaga / Notatka
Kontrakty kodu nie są obsługiwane na platformie .NET 5+ (w tym w wersjach platformy .NET Core). Zamiast tego rozważ użycie typów referencyjnych dopuszczających null.
Kontrakty kodu obejmują klasy oznaczania kodu, analizatora statycznego do analizy czasu kompilacji i analizatora środowiska uruchomieniowego. Klasy kontraktów kodu można znaleźć w System.Diagnostics.Contracts przestrzeni nazw.
Zalety kontraktów kodu obejmują następujące elementy:
Ulepszone testowanie: Kontrakty kodu zapewniają statyczną weryfikację kontraktu, sprawdzanie środowiska uruchomieniowego i generowanie dokumentacji.
Narzędzia do testowania automatycznego: możesz użyć kontraktów kodu, aby wygenerować bardziej znaczące testy jednostkowe, filtrując bezsensowne argumenty testów, które nie spełniają warunków wstępnych.
Weryfikacja statyczna: statyczny moduł sprawdzania może zdecydować, czy istnieją jakiekolwiek naruszenia umowy bez uruchamiania programu. Sprawdza ona umowy niejawne, takie jak odwołania do null i granice tablic oraz umowy jawne.
Dokumentacja referencyjna: Generator dokumentacji rozszerza istniejące pliki dokumentacji XML o informacje o umowie. Istnieją również arkusze stylów, których można używać z platformą Sandcastle , aby strony wygenerowanej dokumentacji miały sekcje kontraktu.
Wszystkie języki programu .NET Framework mogą natychmiast korzystać z kontraktów; nie trzeba pisać specjalnego analizatora ani kompilatora. Rozszerzenie programu Visual Studio umożliwia określenie poziomu analizy kontraktów kodu. Analizatory mogą potwierdzić, że kontrakty są prawidłowo sformułowane (sprawdzanie typów i rozpoznawanie nazw) i mogą utworzyć skompilowany formularz kontraktów w formacie wspólnego języka pośredniego (CIL). Tworzenie kontraktów w programie Visual Studio umożliwia korzystanie ze standardowej funkcji IntelliSense udostępnianej przez narzędzie.
Większość metod w klasie kontraktu jest kompilowana warunkowo; oznacza to, że kompilator emituje wywołania do tych metod tylko wtedy, gdy definiujesz specjalny symbol, CONTRACTS_FULL, przy użyciu #define
dyrektywy . CONTRACTS_FULL umożliwia pisanie kontraktów w kodzie bez używania #ifdef
dyrektyw. Można tworzyć różne kompilacje, niektóre z kontraktami i niektóre bez.
Aby zapoznać się z narzędziami i szczegółowymi instrukcjami dotyczącymi używania kontraktów kodu, zobacz Code Contracts (Kontrakty kodu ) w witrynie marketplace programu Visual Studio.
Warunki wstępne
Warunki wstępne można wyrazić przy użyciu Contract.Requires metody . Warunek wstępny określa stan po wywołaniu metody. Są one zwykle używane do określania prawidłowych wartości parametrów. Wszystkie wymienione w warunkach wstępnych elementy członkowskie muszą być co najmniej tak dostępne jak sama metoda; w przeciwnym razie warunek wstępny może nie być zrozumiany przez wszystkich, którzy wywołują metodę. Warunek nie może mieć skutków ubocznych. Zachowanie w czasie wykonywania nieudanych warunków wstępnych jest określane przez analizator środowiska uruchomieniowego.
Na przykład poniższy warunek wstępny wyraża, że parametr x
musi mieć wartość inną niż null.
Contract.Requires(x != null);
Jeśli twój kod musi zgłosić określony wyjątek w przypadku niepowodzenia warunku wstępnego, możesz użyć uniwersalnego przeciążenia Requires w następujący sposób.
Contract.Requires<ArgumentNullException>(x != null, "x");
Starsza wersja wymaga instrukcji
Większość kodu zawiera weryfikację parametrów if
-then
-throw
w postaci kodu. Narzędzia kontraktowe uznają te instrukcje za warunek wstępny w następujących przypadkach:
Instrukcje są wyświetlane przed wszelkimi innymi instrukcjami w metodzie.
Po całym zestawie takich instrukcji następuje jawne wywołanie metody Contract, takie jak wywołanie metody Requires, Ensures, EnsuresOnThrow lub EndContractBlock.
Gdy if
-then
-throw
instrukcje są wyświetlane w tej formie, narzędzia rozpoznają je jako starsze requires
instrukcje. Jeśli żadne inne kontrakty nie są zgodne z sekwencjąif
-then
-throw
, zakończ kod metodą .Contract.EndContractBlock
if (x == null) throw new ...
Contract.EndContractBlock(); // All previous "if" checks are preconditions
Należy pamiętać, że warunek w poprzednim teście jest negowanym warunkiem wstępnym. (Rzeczywistym warunkiem wstępnym byłoby x != null
.) Negowany warunek wstępny jest bardzo ograniczony: musi być napisany, jak pokazano w poprzednim przykładzie; oznacza to, że nie powinien zawierać else
żadnych klauzul, a treść klauzuli then
musi być pojedynczą throw
instrukcją. Test if
podlega zarówno regułom czystości, jak i widoczności (zobacz Wytyczne dotyczące użycia), ale throw
wyrażenie podlega tylko regułom czystości. Jednak typ zgłaszanego wyjątku musi być tak widoczny, jak metoda, w której występuje kontrakt.
Postwarunki
Warunki końcowe to kontrakty dotyczące stanu metody po jej zakończeniu. Przed zamknięciem metody sprawdzana jest wartość postcondition. Zachowanie wykonania w przypadku niepowodzenia postwarunków jest określane przez analizator czasu wykonania.
W przeciwieństwie do warunków wstępnych, warunki końcowe mogą odwoływać się do członków o mniejszej widoczności. Klient może nie być w stanie zrozumieć lub użyć niektórych informacji wyrażonych po awarii przy użyciu stanu prywatnego, ale nie ma to wpływu na zdolność klienta do prawidłowego używania metody.
Standardowe postwarunki
Standardowe postkondycje można wyrazić za pomocą metody Ensures. Postconditions wyrażają warunek, który musi być spełniony true
po normalnym zakończeniu metody.
Contract.Ensures(this.F > 0);
Wyjątkowe postwarunki
Wyjątkowe warunki końcowe są to warunki końcowe, które powinny być true
, gdy określony wyjątek jest zgłaszany przez metodę. Te wartości końcowe można określić, używając metody Contract.EnsuresOnThrow, jak pokazano w poniższym przykładzie.
Contract.EnsuresOnThrow<T>(this.F > 0);
Argument jest warunkiem, który musi być true
zawsze, gdy jest zgłaszany wyjątek, który jest podtypem T
.
Istnieją pewne typy wyjątków, które są trudne do użycia w wyjątkowej postkondycji. Na przykład użycie typu Exception dla T
wymaga od metody zagwarantowania warunku niezależnie od typu zgłaszanego wyjątku, nawet jeśli jest to przepełnienie stosu lub inny trudny do kontrolowania wyjątek. Należy używać nadzwyczajnych pokondycji tylko w przypadku określonych wyjątków, które mogą być zgłoszone, gdy członek jest wywoływany, na przykład, gdy InvalidTimeZoneException zostanie zgłoszony dla wywołania metody TimeZoneInfo.
Specjalne warunki końcowe
Następujące metody mogą być używane tylko w postwarunkach.
Możesz odwołać się do zwracanych wartości metody w wyrażeniach postcondition przy użyciu wyrażenia
Contract.Result<T>()
, gdzieT
jest zastępowany przez zwracany typ metody. Jeśli kompilator nie może wywnioskować typu, musisz jawnie go podać. Na przykład kompilator języka C# nie może wywnioskować typów metod, które nie przyjmują żadnych argumentów, dlatego wymaga następującej postkondycji:Contract.Ensures(0 <Contract.Result<int>())
Metody z typem zwrotnymvoid
nie mogą odwoływać się doContract.Result<T>()
w swoich postkondycjach.Wartość prestate w pokondycji odnosi się do wartości wyrażenia na początku metody lub właściwości. Używa wyrażenia
Contract.OldValue<T>(e)
, gdzieT
jest typeme
. Można pominąć argument typu ogólnego, gdy kompilator może wywnioskować jego typ. (Na przykład kompilator języka C# zawsze wywnioskuje typ, ponieważ przyjmuje argument). Istnieje kilka ograniczeń dotyczących tego, co może wystąpić we
i kontekstach, w których może pojawić się stare wyrażenie. Stare wyrażenie nie może zawierać innego starego wyrażenia. Co najważniejsze, stare wyrażenie musi odwoływać się do wartości, która istniała w stanie warunków wstępnych metody. Innymi słowy, musi to być wyrażenie, które można ocenić tak długo, jak warunek wstępny metody totrue
. Oto kilka wystąpień tej reguły.Wartość musi istnieć w stanie warunku wstępnego metody. Aby odwołać się do pola w obiekcie, warunki wstępne muszą zagwarantować, że obiekt nigdy nie jest null.
Nie można odwołać się do wartości zwracanej metody w starym wyrażeniu:
Contract.OldValue(Contract.Result<int>() + x) // ERROR
Nie można odwoływać się do
out
parametrów w starym wyrażeniu.Stare wyrażenie nie może zależeć od zmiennej powiązanej kwantyfikatora, jeśli zakres kwantyfikatora zależy od wartości zwracanej metody:
Contract.ForAll(0, Contract.Result<int>(), i => Contract.OldValue(xs[i]) > 3); // ERROR
Stare wyrażenie nie może odwoływać się do parametru delegata anonimowego w wywołaniu ForAll lub Exists , chyba że jest ono używane jako indeksator lub argument do wywołania metody:
Contract.ForAll(0, xs.Length, i => Contract.OldValue(xs[i]) > 3); // OK Contract.ForAll(0, xs.Length, i => Contract.OldValue(i) > 3); // ERROR
Stare wyrażenie nie może wystąpić w treści delegata anonimowego, jeśli wartość starego wyrażenia zależy od któregokolwiek z parametrów delegata anonimowego, chyba że delegat anonimowy jest argumentem metody ForAll lub Exists.
Method(... (T t) => Contract.OldValue(... t ...) ...); // ERROR
Out
parametry stanowią problem, ponieważ kontrakty pojawiają się przed treścią metody, a większość kompilatorów nie zezwala na odwołania doout
parametrów w parametrach postcondition. Aby rozwiązać ten problem, klasa Contract udostępnia metodę ValueAtReturn, która umożliwia postkondycję na podstawie parametruout
.public void OutParam(out int x) { Contract.Ensures(Contract.ValueAtReturn(out x) == 3); x = 3; }
Podobnie jak w przypadku OldValue metody, można pominąć parametr typu ogólnego za każdym razem, gdy kompilator może wywnioskować jego typ. Przepisujący kontrakt zastępuje wywołanie metody na wartość parametru
out
. Metoda ValueAtReturn może pojawić się tylko w postwarunkach. Argument metody musi być parametremout
lub polem parametru strukturyout
. To drugie jest również przydatne podczas odwoływania się do pól w postwarunku konstruktora struktury.Uwaga / Notatka
Obecnie narzędzia do analizy kontraktów kodu nie sprawdzają, czy parametry
out
są inicjalizowane prawidłowo i pomijają ich wzmiankę w warunku końcowym. W związku z tym w poprzednim przykładzie, jeśli wiersz po kontrakcie użył wartościx
zamiast przypisywać do niej liczbę całkowitą, kompilator nie wystawi poprawnego błędu. Jednak w kompilacji, gdzie nie zdefiniowano symbolu preprocesora CONTRACTS_FULL (np. w wersji produkcyjnej), kompilator zgłosi błąd.
Niezmienniki
Niezmienne obiekty to warunki, które powinny być prawdziwe dla każdego wystąpienia klasy, gdy ten obiekt jest widoczny dla klienta. Wyrażają one warunki, w których obiekt jest uznawany za poprawny.
Niezmienne metody są identyfikowane przez oznaczenie za pomocą atrybutu ContractInvariantMethodAttribute . Niezmienne metody nie mogą zawierać kodu z wyjątkiem sekwencji wywołań Invariant metody, z których każda określa pojedynczą niezmienność, jak pokazano w poniższym przykładzie.
[ContractInvariantMethod]
protected void ObjectInvariant ()
{
Contract.Invariant(this.y >= 0);
Contract.Invariant(this.x > this.y);
...
}
Zmienne są warunkowo definiowane przez symbol preprocesora CONTRACTS_FULL. Podczas sprawdzania czasu wykonywania zmienne są sprawdzane na końcu każdej metody publicznej. Jeśli niezmiennik wspomina o metodzie publicznej w tej samej klasie, sprawdzanie niezmiennika, które zwykle odbywa się na końcu tej metody publicznej, jest dezaktywowane. Zamiast tego sprawdzanie odbywa się tylko na końcu najbardziej zewnętrznego wywołania metody do tej klasy. Dzieje się tak również w przypadku ponownego wprowadzenia klasy z powodu wywołania metody w innej klasie. Niezmienne nie są sprawdzane pod kątem finalizatora obiektów i implementacji IDisposable.Dispose .
Wskazówki dotyczące użycia
Zamawianie kontraktów
W poniższej tabeli przedstawiono kolejność elementów, których należy użyć podczas pisania kontraktów metod.
If-then-throw statements |
Warunki wstępne publiczne zgodne z poprzednimi wersjami |
---|---|
Requires | Wszystkie publiczne warunki wstępne. |
Ensures | Wszystkie publiczne (normalne) postkondycje. |
EnsuresOnThrow | Wszystkie publiczne warunki wyjątkowe postwarunki. |
Ensures | Wszystkie prywatne/wewnętrzne (standardowe) warunki końcowe. |
EnsuresOnThrow | Wszystkie prywatne/wewnętrzne wyjątkowe warunki końcowe. |
EndContractBlock | Jeśli używasz warunków wstępnych stylu if -then -throw bez żadnych innych kontraktów, umieść wywołanie EndContractBlock, aby wskazać, że wszystkie poprzednie instrukcje warunkowe są warunkiem wstępnym. |
Czystość
Wszystkie metody wywoływane w ramach kontraktu muszą być czyste; oznacza to, że nie mogą aktualizować żadnego istniejącego stanu. Czysta metoda może modyfikować obiekty, które zostały utworzone po jej wywołaniu.
Narzędzia kontraktów kodu obecnie zakładają, że następujące elementy kodu są czyste:
Metody oznaczone znakiem PureAttribute.
Typy oznaczone znakiem PureAttribute (atrybut ma zastosowanie do wszystkich metod typu).
Właściwość pobiera metody dostępu.
Operatory (metody statyczne, których nazwy zaczynają się od "op", i które mają jeden lub dwa parametry i niepusty typ zwracany).
Każda metoda, której w pełni kwalifikowana nazwa zaczyna się od "System.Diagnostics.Contracts.Contract", "System.String", "System.IO.Path" lub "System.Type".
Każdy wywoływany delegat, pod warunkiem, że sam typ delegata jest opatrzony atrybutem PureAttribute. Typy delegatów System.Predicate<T> i System.Comparison<T> są uważane za czyste.
Widoczność
Wszyscy członkowie wymienieni w umowie muszą być co najmniej tak widoczni, jak sposób ich przedstawienia. Na przykład nie można wspomnieć o polu prywatnym w predefiniowanym warunku dla metody publicznej; użytkownicy nie mogą zweryfikować takiego kontraktu przed wywołaniem metody. Jeśli jednak pole jest oznaczone znakiem ContractPublicPropertyNameAttribute, jest wykluczone z tych reguł.
Przykład
W poniższym przykładzie pokazano użycie kontraktów kodu.
#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