Udostępnij za pośrednictwem


Kontrakty kodu (.NET Framework)

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:

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>(), gdzie T 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 zwrotnym void nie mogą odwoływać się do Contract.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), gdzie T jest typem e. 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ć w e 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 to true. 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 do out parametrów w parametrach postcondition. Aby rozwiązać ten problem, klasa Contract udostępnia metodę ValueAtReturn, która umożliwia postkondycję na podstawie parametru out.

      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ć parametrem out lub polem parametru struktury out . 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ści x 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