Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Замечание
Эта статья связана с .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