Контракты кода (платформа .NET Framework)

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

Примечание.

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

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

Среди преимуществ контрактов для кода можно назвать следующие.

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

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

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

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

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

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

Средства и подробные инструкции по использованию контрактов кода см. на сайте Visual Studio Marketplace.

Предварительные условия

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

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

Contract.Requires(x != null);

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

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

Устаревшие операторы Requires

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

Contract.Ensures(this.F > 0);

Исключительные постусловия

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

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

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

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

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

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

  • Вы можете ссылаться на возвращаемые значения метода в постусловиях с помощью выражения Contract.Result<T>(), где T заменяется типом возвращаемого значения метода. Когда компилятор не может вывести тип, необходимо предоставить его явно. Например, компилятор C# не может вывести типы для методов, которые не принимают аргументы, поэтому для него требуется следующее постусловие: 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в постусловиях. Для разрешения этой проблемы класс 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, чтобы указать, что все предыдущие проверки if являются предусловиями.

Чистота

Все методы, которые вызываются в контракте, должны быть чистыми, то есть они не должны обновлять никакие предварительно существующие состояния. Чистому методу разрешается изменять объекты, которые были созданы после входа в этот чистый метод.

В настоящее время средства контрактов для кода предполагают, что чистыми являются следующие элементы кода.

  • Методы, помеченные атрибутом PureAttribute.

  • Типы, помеченные атрибутом PureAttribute (этот атрибут относится ко всем методам типа).

  • Методы доступа get свойства.

  • Операторы (статические методы, имена которых начинаются с op, имеющие один или два параметра и тип возвращаемого значения, отличный от void).

  • Любой метод, полное имя которого начинается с System.Diagnostics.Contracts.Contract, System.String, System.IO.Path или System.Type.

  • Любой вызванный делегат, при условии, что сам тип этого делегата помечен атрибутом PureAttribute. Типы делегата System.Predicate<T> и System.Comparison<T> считаются чистыми.

Visibility

Все члены, упомянутые в контракте, должны быть не менее видимы, чем метод, в котором они появляются. Например, закрытое поле не может упоминаться в предусловии для открытого метода; клиенты не могут проверить такой контракт до вызова метода. Однако если это поле помечено атрибутом 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