Code Contracts library for .NET 3.5: introduction in Russian. Code Contracts provide a language-agnostic way to express coding assumptions in .NET programs. The contracts take the form of pre-conditions, post-conditions, and object invariants and act as checked documentation of your external and internal APIs.
Контрактное программирование – это метод проектирования программ, предполагающий четкое специфицирование интерфейсов и описание обязанностей компонентов системы при взаимодействии друг с другом.
Обычно, контракт включает три основные группы элементов:
- предусловия – обязательства, которые должны быть выполнены вызывающей стороной перед вызовом метода
- постусловия – обязательства, гарантирующиеся вызываемым методом
- инварианты класса – обязательства, что свойства класса будут удовлетворять определенным требованиям. Например, инвариантом для класса PositiveNumber, может быть утверждение, что хранимое число всегда больше или равно 0.
Таким образом, этот метод обеспечивает уверенность вызываемого компонента, что он получит только надлежащие входные данные, а результат, возвращенный вызывающему компоненту, будет корректным.
На PDC2008 было анонсировано появление в .NET 4.0 набора функций реализующих поддержку контрактного программирования и входящих в новое пространство имен System.Diagnostics.Contracts. На практике это будет выглядеть так:
class Account
{
public double Balance { get; set; }
public void Deposit(double amount)
{
Contract.Requires(amount >=0);
Balance += amount;
}
public void Withdraw(double amount)
{
Contract.Requires(amount > 0);
Contract.Requires(amount <= Balance);
Balance -= amount;
}
public Account Transfer()
{
Contract.Ensures(Balance == 0);
Contract.Ensures(Contract.Result() != null);
Account newAccount = new Account() {Balance = this.Balance};
Balance = 0;
return newAccount;
}
[ContractInvariantMethod]
protected void ValidAccount()
{
Contract.Invariant(Balance >=0);
}
}
Как можно увидеть, контракт реализуется при помощи функций, что сразу вызывает вопрос, почему? Гораздо красивее была бы объявление контракта отдельно от кода, например с помощью атрибутов. К сожалению, атрибуты, по утверждению разработчиков, не позволяют описать все сценарии использования контрактов. Была возможность передавать в атрибуты сложные выражения, но они не захотели дублировать функциональность синтаксического анализатора. Так же они не захотели добавлять эту функциональность напрямую в язык, так как при этом другие .NET-языки не смогли бы ею воспользоваться. Так что мы имеем набор из десятка функций и нескольких атрибутов, которые войдут в mscorlib
.
Code Contracts library
.NET 4 пока что не вышел, но попробовать контрактное программирование можно уже сейчас, загрузив библиотеку Code Contracts для .NET 3.5 с сайта Dev Labs. Библиотека доступна в двух вариантах: VSTS Edition для владельцев Visual Studio Team System, в которой доступны все возможности и ее применение разрешено в коммерческих целях, и Standard Edition для всех версий Visual Studio, за исключением Express Edition. Эта версия лицензируется для академических целей и не включает проверки контрактов во время компиляции.
При инсталляции, в систему добавляется библиотека Microsoft.Contracts, примеры и документация. Также, в Visual Studio инсталлятор добавляет в свойства проекта дополнительную страницу Code Contracts, на которой можно управлять поведением библиотеки. Для того, что бы начать работу с контрактами, в проект необходимо добавить ссылку на библиотеку Microsoft.Contracts.dll
, расположенную в папке %PROGRAMFILES%/Microsoft/Contracts/PublicAssemblies
.
Код, приведенный выше для примера, демонстрирует использование трех основных частей контракта: предусловий Contract.Requires в функциях Deposit и Withdraw, декларирующих, что они работают только с положительными суммами денег; постусловий Contract.Ensures в функции Transfer, которые заявляют, что в качестве результата функция должна вернуть новый счет и обнулить текущий; инварианта класса ValidAccount, утверждающего, что счет находиться в правильном состоянии, если его баланс больше или равен нулю.
Итак, контракт объявлен. Но что это нам дает? Что произойдет, если контракт будет нарушен, например, в случае такого вызова account.Deposit(-1000.0)? Это зависит от используемых настроек. Если включена статическая проверка контрактов, то уже на этапе компиляции будет выдано предупреждение о нарушении контракта. Если же включена только проверка контрактов во время выполнения, то, в простейшем случае, в момент вызова функции будет сгенерировано исключение ContractException. Если проверка отключена полностью, то никакой реакции не последует, так как в скомпилированном коде будут отсутствовать любые проверки.
- На самом деле код не просто скомпилирован, но и пропущен через модуль ccrewrite.exe, который подвергает полученный в результате компиляции IL-код постобработке, в результате чего декларация контракта превращается в конкретные вызовы методов проверки.
Дополнительно, библиотека позволяет достаточно гибко менять параметры runtime-проверки контрактов. Во-первых, можно задать какие именно части контракта необходимо проверять, например, только предусловия. И, во-вторых, можно перехватить событие Contract.ContractFailed или задать свой класс, методы которого будут вызываться вместо генерации исключения. Таким образом, к примеру, можно автоматически протоколировать нарушения контракта перед завершением программы.
Далее коротко пройдемся по всем основным элементам контракта.
Предусловия
Декларируют условия, необходимые для работы метода, выражаются с помощью Contract.Requires() и обычно используются для проверки входящих параметров. Функция Requires()принимает в качестве параметра логическое выражение выражающее условие контракта. Так же существует перегруженная функция, принимающая в качестве второго параметра строку – сообщение, используемое при нарушении условия.
Contract.Requires(amount > 0, "Invalid amount");
Как уже упоминалось выше, в зависимости от настроек текущей конфигурации в скомпилированном коде проверок контракта может и не быть. Однако, если вы хотите, что бы проверка предусловия осуществлялась всегда, вне зависимости от настроек, то можно использовать функцию Contract.RequiresAlways(), которая будет работать даже при отключеннии проверок в настройках проекта.
Contract.RequiresAlways(x != 0);
Если вы добавляете поддержку контрактов в существующее приложение, то, возможно, у вас уже есть проверка входящих параметров функций. Если она выполнена в виде
if (условие) throw new...
то, добавив вызов Contract.EndContractBlock() после этих проверок, вы превратите их в предусловия. Аналогичное действие окажет и вызов Contract.Requires(). На выражения накладываются следующие ограничения – они не должны выполнять ничего кроме генерации исключения, оператор else не поддерживается.
Постусловия
Постусловия декларируют состояние, достигнутое после завершения работы метода. Простой пример постусловия:
Contract.Ensures(Balance == 0);
Постусловия, как и предусловия, объявляются в начале метода, однако в результате постобработки проверка условия будет осуществлена после выполнения всех инструкций метода. Это легко увидеть, запустив отладку метода содержащего постусловия – сначала отладчик пройдет по всем инструкциям, включая return и лишь затем перейдет к выполнению проверок.
Приведенный выше синтаксис работает в простейших случаях, для реализации более сложных сценариев используются дополнительные методы.
Так, для верификации возвращаемого функцией значения используется специальный метод Contract.Result(), где T – это тип возвращаемого функцией значения.
Contract.Ensures( Contract.Result() > 0 );
Метод Contract.OldValue(e) позволяет получить значение переменной e до начала работы метода. Существует несколько ограничений на применение OldValue, главное из которых – переменная должна существовать перед началом выполнения инструкций метода и ее значение должно быть вычисляемым. В качестве таких переменных, например, могут выступать свойства класса или входящие переменные метода.
Contract.Ensures(Contract.OldValue(i) < n);
Для проверки в постусловиях значений out-переменных используется метод Contract.ValueAtReturn(out T t)
public void TestOut (out int a)
{
Contract.Ensures(Contract.ValueAtReturn(out a) == 8);
a = 8;
}
Инварианты класса
Инварианты класса относятся ко всему экземпляру класса и декларируют условия, при которых объект находиться в “хорошем” состоянии.
[ContractInvariantMethod]
protected void ValidAccount()
{
Contract.Invariant(Balance >= 0);
}
Объявления всех инвариантов помещаются в один метод класса, помеченный атрибутом ContractInvariantMethod. Этот метод не должен содержать никакого кода, кроме объявлений инвариантов и не может возвращать значения. Каждый инвариант задается вызовом метода Contract.Invariant(). Проверка инвариантов класса происходит в конце каждого public метода класса – если посмотреть IL-код программы, то можно увидеть, что в процессе постобработки в конец каждого такого метода добавляется вызов нашей функции, помеченной атрибутом ContractInvariantMethod.
Дополнительно, в любом месте программы можно использовать Contract.Assert() для проверки условия в этой точке.
Contract.Assert( _number > 0 );
В процессе выполнения программы, если выражение ложно, то произойдет вызов Debug.Assert, в противном случае не последует никаких действий.
Контракты для интерфейсов
Описанные выше методы декларации контракта отлично работают в случае с конкретным классом. Однако что нам делать, если возникнет необходимость задать контракт для интерфейса? Ведь компилятор C# не позволит добавить для интерфейса тело функции с вызовом методов Contract.Requires() или Contract.Ensures(). На помощь приходит вспомогательный класс и пара атрибутов – ContractClass и ContractClassFor.
[ContractClass(typeof(SavingAccount))]
interface IAccount
{
int Balance { get; }
void Deposit(int amount);
}
[ContractClassFor(typeof(IAccount))]
class SavingAccount : IAccount
{
int IAccount.Balance
{
get
{
Contract.Ensures(Contract.Result() >=0 );
return default(int);
}
}
void IAccount.Deposit(int amount)
{
Contract.Requires(amount > 0);
}
}
Для интерфейса создается вспомогательный класс, реализующий его и декларирующий контракт. Для установления связи между классом и интерфейсом используются атрибуты. Точно таким же способом можно объявить контракт для абстрактного метода, создав вспомогательного наследника и реализовав метод.
Поскольку статья задумывалась как введение в библиотеку Code Contracts, то я не стал описывать некоторые нюансы ее использования и методы, используемые в специальных случаях – с ними вы можете познакомиться в достаточно подробном документе, идущем вместе с библиотекой. Так же, для начала, может быть интересным видео с одним из разработчиков библиотеки, доступное на сайте Dev Labs.