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.

Контрактное программирование – это метод проектирования программ, предполагающий четкое специфицирование интерфейсов и описание обязанностей компонентов системы при взаимодействии друг с другом.

Обычно, контракт включает три основные группы элементов:

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

На 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. Эта версия лицензируется для академических целей и не включает проверки контрактов во время компиляции.

CodeContracts

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

Дополнительно, библиотека позволяет достаточно гибко менять параметры 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.

blog comments powered by Disqus