Поделиться через


Контракты кода (.NET Framework)

Замечание

Эта статья связана с .NET Framework. Он не применяется к более новым реализациям .NET, включая .NET 6 и более поздние версии.

Контракты кода предоставляют способ указания предварительных условий, посткондиционных и инвариантных объектов в коде .NET Framework. Предварительные условия — это требования, которые должны быть выполнены при вводе метода или свойства. Постусловия описывают ожидания в момент завершения кода метода или свойства. Инварианты объекта описывают ожидаемое состояние для класса, который находится в хорошем состоянии.

Замечание

Контракты кода не поддерживаются в .NET 5+ (включая версии .NET Core). Вместо этого рекомендуется использовать ссылочные типы, допускающие значение NULL .

Контракты кода включают классы для маркировки кода, статического анализатора для анализа во время компиляции и анализатора среды выполнения. Классы для контрактов кода можно найти в System.Diagnostics.Contracts пространстве имен.

К преимуществам контрактов кода относятся следующие преимущества:

  • Улучшенное тестирование: контракты кода обеспечивают статическую проверку контракта, проверку среды выполнения и создание документации.

  • Средства автоматического тестирования: вы можете использовать контракты кода для создания более значимых модульных тестов, отфильтровав бессмысленные аргументы теста, которые не удовлетворяют предварительным условиям.

  • Статическая проверка: статический средство проверки может решить, существуют ли нарушения контракта без запуска программы. Он проверяет неявные контракты, такие как разыменование null и проверка границ массива, а также явные контракты.

  • Справочная документация. Генератор документации расширяет существующие XML-файлы документации с сведениями о контракте. Существуют также таблицы стилей, которые можно использовать с Sandcastle , чтобы созданные страницы документации имели разделы контракта.

Все языки .NET Framework могут немедленно воспользоваться преимуществами контрактов; Вам не нужно писать специальный анализатор или компилятор. Надстройка Visual Studio позволяет указать уровень анализа контрактного кода, который будет выполняться. Анализаторы могут подтвердить, что контракты корректно сформированы (проверка типов и разрешение имен) и могут создавать скомпилированную форму контрактов в формате общего промежуточного языка (CIL). Создание контрактов в Visual Studio позволяет воспользоваться преимуществами стандартной IntelliSense, предоставляемой средством.

Большинство методов в классе контракта компилируются условно; То есть компилятор выдает вызовы этих методов только при определении специального символа, CONTRACTS_FULL с помощью директивы #define . CONTRACTS_FULL позволяет создавать контракты в коде без использования #ifdef директив; вы можете создавать различные сборки, некоторые с контрактами и без нее.

Для получения инструментов и подробных инструкций по использованию контрактов кода см. Code Contracts на сайте Visual Studio Marketplace.

Предпосылки

Вы можете выразить предварительные условия с помощью Contract.Requires метода. Предварительные условия указывают состояние при вызове метода. Обычно они используются для указания допустимых значений параметров. Все члены, упомянутые в предварительных условиях, должны быть по крайней мере такими же доступными, как и сам метод; в противном случае предварительные условия могут не быть поняты всеми вызывающими метод. Условие не должно иметь побочных эффектов. Поведение во время выполнения неудачных предварительных условий определяется анализатором среды выполнения.

Например, следующее условие выражает, что параметр x должен быть не null.

Contract.Requires(x != null);

Если код должен вызвать определенное исключение при сбое предварительных условий, можно использовать универсальную перегрузку Requires , как показано ниже.

Contract.Requires<ArgumentNullException>(x != null, "x");

Требуемые заявления о наследии

Большинство кода содержит некоторые проверки параметров в виде if-then-throw кода. Средства контракта признают эти заявления предварительными условиями в следующих случаях:

  • Операторы отображаются перед любыми другими операторами в методе.

  • За всем набором таких инструкций следует явный вызов метода Contract, например вызов метода Requires, метода Ensures, метода EnsuresOnThrow или метода EndContractBlock.

Когда 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. Postconditions выражает условие, которое должно быть true при нормальном завершении метода.

Contract.Ensures(this.F > 0);

Особые постусловия

Исключительные постусловия — это постусловия, которые должны быть true в случае, если методом выброшено определенное исключение. Эти postconditions можно указать с помощью Contract.EnsuresOnThrow метода, как показано в следующем примере.

Contract.EnsuresOnThrow<T>(this.F > 0);

Аргумент — это условие, которое должно быть true всякий раз, когда создается исключение, которое является подтипом T .

Некоторые типы исключений трудно использовать в постусловии исключения. Например, для использования типа ExceptionT требуется, чтобы метод гарантировал условие независимо от типа исключения, которое возникает, даже если это переполнение стека или другое невозможное исключение для управления. Следует использовать исключительные постусловия только для конкретных исключений, которые могут возникнуть при вызове члена, например, когда InvalidTimeZoneException вызывается для метода TimeZoneInfo.

Специальные постусловия

Следующие методы могут использоваться только в постусловиях:

  • Возвращаемые значения метода можно использовать в постусловиях с помощью выражения Contract.Result<T>(), где T заменяется возвращаемым типом метода. Если компилятору не удается определить тип, необходимо явно указать его. Например, компилятор C# не может выводить типы методов, которые не принимают никаких аргументов, поэтому для этого требуется следующее postcondition: Contract.Ensures(0 <Contract.Result<int>()) методы с возвращаемым типом void не могут ссылаться 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 параметры в postconditions. Чтобы решить эту проблему, класс 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 (например, релизная сборка), возникает ошибка компиляции.

Инварианты

Инварианты объектов — это условия, которые должны быть верными для каждого экземпляра класса, когда этот объект отображается клиенту. Они выражают условия, при которых объект считается правильным.

Инвариантные методы определяются пометкой атрибутом 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" и которые имеют один или два параметра и тип возвращаемого значения отличный от 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