Контракт – это по сути спецификация компонентов системы. Вот как определили Контрактное программирование в википедии:
Контрактное программирование — это метод проектирования программного обеспечения. Он предполагает, что проектировщик должен определить формальные, точные и верифицируемые спецификации интерфейсов для компонентов системы.
Также как понимает это сам создатель можно прочитать в его интервью.
Итак зачем они нужны?
Справедливости ради, следует заметить, что Code contracts достаточно давно существуют как проект в рамках Microsoft Research, но с приходом .NET 4.0 для него наступил звездный час и его внесли в CLR. Хотя мне кажется это вполне логичным развитием как Debug.Assert(Trace.Assert) и if-then-throw контрактов которые появились вместе с самим C#, так и определенная популяризация идея defensive programming.
CLR без сode contracts располагает похожим механизмом. Казалось бы, все что привносят сode contracts можно было реализовать до этого с помощью
Debug.Assert, Trace.Assert,
или throw
. Но тем не менее код с сode contracts выглядит намного чище, и прежние ассерты не давали возможности проводить статическую проверку на этапе компиляции, не было возможности создавать автоматически сгенерированы юнит-тесты, не было возможности сгенерировать документацию для объекта с учетом тех спецификаций, которые они накладывают на объект. Теперь же с появлением code contracts все это возможно.Давайте посмотрим насколько это возможно.
И хотя сам класс
System.Diagnostics.Contracts.Contract
включили в состав .NET 4.0, тем не менее, чтобы его использовать необходимо придется либо вручную определить в свойствах проекта символ CONTRACTS_FULL для проверки контрактов в runtime либо загрузить с сайта проекта tools, которые добавят в свойства проекта новую вкладку с названием code contracts. В Visual Studio 2010, релизном варианте, скачанном после запуска, такая вкладка отсутствует.
Давайте попробуем, как ведет себя статическая проверка. Достаточно на этой вкладке отметить чекбокс “Perform static checking” и скомпилировать код с контрактами и/или нарушением этих контрактов. В итоге все проверки появятся, как warnings если было нарушение.

Тестируемость. Например, можно создать автосгенерированные юнит-тесты с помощью такого инструмента как Pex (http://research.microsoft.com/en-us/projects/pex/). На скриншоте pex вывел результат теста, который не прошел проверку по контракту:

Для генерирования документации существует утилита Code Contract Document Generator Tool (CCDocGen.exe). Она добавляет информацию про контракты в уже сгенерированные компилятором файлы XML документации, а чтобы из XML файла получить, например, документацию в стиле MSDN достаточно воспользоваться утилитой SandCastle (http://sandcastle.codeplex.com/). Хотя помимо сгенерированой документации, сам код является в некотором роде самодокументированым. Т.е. достаточно одним глазом взглянуть на контракты чтобы определить каким требованиям должны соотвествовать объекты.
Итак, преимуществ перед старыми средствами достаточно много.
Code contracts есть 3 видов:
- Preconditions – используется для валидации аргументов
- Postconditions – для проверки состояния по завершению метода, независимо от того нормально он завершился или с исключением
- Object invariants – для проверки, что данные объекта находятся в хорошем состоянии на протяжении жизни объекта
Если одно из этих условий нарушается, то при runtime проверке мы получим
ContractException
. Существует также возможность подписаться на событие Contract.ContractFailed
чтобы либо продолжить/прервать выполнение, либо отреагировать на нарушение контракта. Например подписку на ContractFailed
можно использовать, например для того чтобы юнит-тесты не остановили процесс сборки тестов на билд машине. Если контракт будет нарушен в runtime то мы увидим стандартный для ассертов диалог.

Контракты очень похожи на ассерты поэтому для их понимания давайте лучше по смотрим на них в действии. Итак, пусть у нас есть объекты
Order
и OrderItem
.public class Order
{
List<OrderItem> orderedItems = new List<OrderItem>();
decimal orderPrice = 0;
public void MakeNewOrder(OrderItem orderItem)
{
// precondition
Contract.Requires<ArgumentNullException>(orderItem != null);
Contract.Requires(Contract.ForAll(orderedItems, p => p != orderItem));
// postcondition
Contract.Ensures(Contract.Exists(orderedItems, p => p == orderItem));
Contract.Ensures(orderPrice > Contract.OldValue(orderPrice));
orderedItems.Add(orderItem);
orderPrice += orderItem.Price;
}
[ContractInvariantMethod]
private void ObjectInvariant()
{
Contract.Invariant(orderPrice > 0);
}
}
Аналогом такого класса с контрактами может быть класс выполненный с контрактами в стиле if-then-throw:public class OrderOld
{
List<OrderItem> orderedItems = new List<OrderItem>();
decimal orderPrice = 0;
public void MakeNewOrder(OrderItem orderItem)
{
// precondition
if (orderItem == null)
throw new ArgumentNullException("orderItem", "orderItem is null.");
if (orderedItems.Any(p => p == orderItem))
throw new InvalidOperationException("Item already ordered");
Contract.EndContractBlock();
decimal oldPrice = orderPrice;
orderedItems.Add(orderItem);
orderPrice += orderItem.Price;
// postcondition
if (!orderedItems.Any(p => p == orderItem))
throw new InvalidOperationException("Item wasn't added");
if (orderPrice <= oldPrice)
throw new InvalidOperationException("Item's price invalid");
//invariant
if (orderPrice < 0)
throw new InvalidOperationException("Order's price invalid");
}
}
Предусловия(preconditions) реализованы используя Contract.Requires
и специфицируют, что OrderItem
не должен быть null или уже заказан. Пост условия - Contract.Ensures
, что OrderItem
должен быть заказан и цена заказа должна возрасти. И есть еще инвариант. Инвариант реализуются с помощью методов с атрибутом ContractInvariantMethod
при чем их может несколько но в итоге они будут объединены. Так вот, инварианты будут вызваны в конце вызова каждого public
метода экземпляра класса. Условия используемые в preconditions, preconditions и invariants не должны изменять состояние объекта. Метод Contract.Requires<ArgumentNullException>
например в случае неверного контракта выбросит соотвествущее исключение.Как это работает? Специальная утилита Code contract rewriter tool (CCRewrite.exe) модифицирует IL код, так что предусловия будут выполняться в начале метода, пост – в конце, а инварианты – после каждого публичного. Эта утилита не модифицирует только методы
Finalize
или Dispose
.Среди прочих полезных методов класса
Contract
:Contract.Requires<ArgumentNullException>(x != null, “x”)
– используется для того чтобы если предусловие не прошло проверки вызвать соотвествующее исключениеContract.EnsuresOnThrow
– постусловие, если метод завершится указанным исключениемContract.Result<int>()
– используется если в контракте понадобится сравнить с возвращаемым значением методаContract.OldValue(xs.Length)
– используется для сравнения с предыдущим значением в контрактеContract.ForAll
– можно также накладывать контракты на диапазоны(списки) значенийContract.ValueAtReturn(out x)
– помагает для out значенийContract. Exists
– если нужно проверить на существование в спискеContract.Equals
– проверка на равенствоContract.EndContractBlock
– для того чтобы контракты «старого» вида if-then-throw распознавались как code contracts
if (orderItem == null)
throw new ArgumentNullException("orderItem", "orderItem is null.");
if (orderedItems.Any(p => p == orderItem))
throw new InvalidOperationException("Item already ordered");
Contract.EndContractBlock();
А также Contract.Assume
и Contract.Assert
. Contract.Assert
это аналог Debug.Assert(Trace.Assert)
с одной лишь разницей, что при использовании варианта из code contracts мы получаем статическую проверку. Условие в Contract.Assume
в отличии от Contract.Assert
не проверяется, а сохраняется как истинное для того чтобы помочь в статической проверке контрактов.Также контракты можно использвать с интерфейсами и абстрактными классами, правда так как методы в них могут не иметь тела поэтому контракты для них пишутся в отдельном классе, а итерфейс/абстрактный класс и класс с их контрактами связываются между собой атрибутами
ContractClass
и ContractClassFor
: [ContractClass(typeof(OrderItemContract))]
public interface IOrderItem
{
string ItemName { get; set; }
decimal Price { get; set; }
}
[ContractClassFor(typeof(IOrderItem))]
sealed class OrderItemContract : IOrderItem
{
private string itemName;
public string ItemName
{
get
{
Contract.Ensures(!string.IsNullOrEmpty(itemName));
return itemName;
}
set
{
itemName = value;
}
}
private decimal price;
public decimal Price
{
get
{
return price;
}
set
{
Contract.Requires(value > 0);
price = value;
}
}
}
Контракты также наследуются. Правда действуют определенные правила:- В наследниках нельзя определить предусловия, предусловия определяются только в базовом классе
- В наследниках можно усилить постусловия
- Инварианты наследника используются вместе с инвариантами базового класса для верификации объекта наследника.
А что если используемые методы контрактов в CLR нас чем-то не устраивают, либо хочется их дополнить. Существует механизм для этого – custom contracts или custom rewriters methods. Суть этого механизма в том что мы можем сами определить эти методы, например, в отдельной сборке, а потом указать этот contract runtime class и/или сборку в которой он лежит либо используя інтерфейс(вкладку code contracts -> custom rewriters methods можно увидеть на скриншоте с вкладкой) либо опции командной строки. Методы которые нужно переопределить должны иметь следующий вид:
public static class RuntimeFailureMethods
{
public static void Requires(bool cond, string userMsg, string condText)
{ /*...*/ }
public static void Requires<E>(bool cond, string userMsg, string condText)
where E : Exception
{ /*...*/ }
public static void Ensures(bool cond, string userMsg, string condText)
{ /*...*/ }
public static void EnsuresOnThrow(bool cond, string userMsg, string condText, Exception innerException)
{ /*...*/ }
public static void Assert(bool cond, string userMsg, string condText)
{ /*...*/ }
public static void Assume(bool cond, string userMsg, string condText)
{ /*...*/ }
public static void Invariant(bool cond, string userMsg, string condText)
{ /*...*/ }
public static void ReportFailure(ContractFailureKind kind, string userMsg, string condText, Exception inner)
{ /*...*/ }
public static string RaiseContractFailedEvent(ContractFailureKind kind, string userMsg, string condText, Exception inner)
{ /*...*/ }
public static void TriggerFailure(string message, string userMsg, string condText, Exception inner)
{ /*...*/ }
}
Если какие-либо методы пропущены, то они либо будут синтезированы либо исопользованы стандартные методы. Класс с custom rewriters methods может лежать как в отдельной сборку так и в основной, главное чтоб rewriter смог его найти.Code contracts несомненно полезная штука, которую будут использовать и возможно в будущем она полностью заменит
Debug.Assert/Trace.Assert
.