.NET

индекс
121,07

Code Contracts в .NET 4.0

В .NET 4.0 в рамках CLR появилась такая новинка как Code Contracts. Что оно такое? Code Contracts это развитие идеи программирования по контракту (Design by Contract), которая была введена Бертраном Мейером, создателем языка Эйфель. Чтобы услышать объяснение того что такое контракты и как они улучшают разработку программного обеспечения можно почитать его интервью.

Контракт – это по сути спецификация компонентов системы. Вот как определили Контрактное программирование в википедии:

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

Также как понимает это сам создатель можно прочитать в его интервью.

Итак зачем они нужны? Для того чтобы проверить или верифицировать объекты и компоненты системы, при этом улучшая отладку, тестирование, документирование.

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

image

Давайте попробуем, как ведет себя статическая проверка. Достаточно на этой вкладке отметить чекбокс “Perform static checking” и скомпилировать код с контрактами и/или нарушением этих контрактов. В итоге все проверки появятся, как warnings если было нарушение.

image

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

image

Для генерирования документации существует утилита 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 то мы увидим стандартный для ассертов диалог.

image

Контракты очень похожи на ассерты поэтому для их понимания давайте лучше по смотрим на них в действии. Итак, пусть у нас есть объекты 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.
+19
17 апреля 2010, 02:41
49

комментарии (27)

+1
SychevIgor #
Идеология code contract мне очень нравится… Контроль кода и все такое. Из личного опыта есть ограничения для того, как я бы хотел использовать:

Код в стиле if else throw быстрее чуть ли не в двое чем code contract.(для 99% вариантов использования- это не критично)
И еще меня очень сильно огорчило, невозможность использования code contract с массивами, коллекциями или в операторах условия. Поясню зачем: допустим метод принимает в качестве параметра массив. он должен быть либо 4int либо 6int. для все одинаковая проверка, что они не более чем 255. В итоге я не могу написать проверку в цикле, а вынужден писать 4 предусловия а затем оставшиеся 2 проверять if else стилем. что не логично и целостно.
Можно было бы попробовать сделать 4 проверки codecontract всегда и 2 по условия(если длинна массива равна 6), но по условия тоже выполнить проверку мне не кто не даст…

Так что все хорошо но как валидировать коллекции не понятно. А вообще проект мне нравится
+1
chaliy #
Эм… Для работы с масивами и коллекциями есть Contract.For, для него пока что не работает статический анализ, но с рантайм проверкой все ок. Скоро заимплементят и статический анализ.
+1
Regfor #
Да, для масивов и коллекций есть Contract.ForAll. Я думаю вам подошел бы такой контракт:

public void ArrayTest(params int[] numbers)
{
Contract.Requires(Contract.ForAll(numbers, p => p < 255));
//…
}
+1
mezastel #
Что-то слишком много фич команда VS не успела правильно сделать до релиза.

За пост спасибо.
0
ControlFlow #
Я бы не сказал, что CC — именно фича, изначально планирующаяся для включения в VS2010.

Совсем недавно это был MS Research проект, теперь переехал на DevLabs, но до продакшена ему далековато ещё, они там такой ужас эмитят, что почти на два порядка медленнее if-then-throw проверки.

Я вот недоумеваю зачем было в BCL 4.0 включать абсолютно бесполезный нэймспейс System.Diagnostics.Contracts, методы типов которого бросают исключения без реврайта CC.
0
mezastel #
Нет, у меня было впечатление что СС как раз будет фичей 10/4. Наверное я ошибался. То же самое с UML — я надеялся что будет roundtrip.
0
chaliy #
Включили по той же причине что IObservable, для того чтобы ты мог распространять либы без зависимостей. Например если распространяеш контрактную версию либы.
+1
webus #
Жаль на Visual C# Express, Code Contracts Tools не работают (
+2
chaliy #
Что уж там переживать за Express. Они полностью работают тока на Premium или Ultimate Edition. Во всех остальных нема статического анализатора.
+1
Regfor #
Да, как бы, и в релизной версии Ultimate 2010 версии, например, в свойствах проекта нет такой вкладки для контрактов. Т.е. включить статическую проверку, которая выключена по умолчанию проблематично.
0
chaliy #
Нет вкладки после установки Code Contracts Tools? Статический анализ есть тока в тулзах. В фреймворк ни статический анализ, ни код реврайт не входят.
+2
XaocCPS #
>> В .NET 4.0 появилась такая новинка как Code Contracts

Хотя я понял автора, для других стоит пояснить, что Code Contracts были доступны давно для .NET 3.5 как отдельно устанавливаемое дополнение.
+1
Regfor #
Да, действительно, я уточнение в текст
0
luminous #
Спасибо за статью. Было бы интересно увидеть еще использование «Custom Rewriter Methods», и упустили момент с указанием типа генерируемого исключения, например — Contract.Requires<ArgumentNullException>(orderItem != null).
+1
Regfor #
Действительно было бы лучше использовать Contract.Requires<ArgumentNullException>(orderItem != null), я изменил в статьие и добавил упоминание.

Добавил в конце статьи еще немного о Custom Rewriter Methods.
0
Hanhe #
Было бы классно, если бы задавать контракт можно было бы в декларативном виде, например как атрибуты.
+2
chaliy #
Компилтайм проверки теряется. Тоесть там фитча в том что вы например пишете arg.Length > 0 или arg != null… а вот если бы это делалось при помощи атрибутов то пришлось бы использовать множество строк, типа названия агрумента. Кстати если тема интересна поглядите на SpecSharp, это расширение C# которое декларативно позволяет описывать контракты.
+1
Hanhe #
Да нет, я понимаю, что атрибуты не приспособлены для этого, я имел в виду, что было бы удобно указывать контракты так, как указываются атрибуты. Это имелю бы целый ряд плюсов:
Это более логично, чем писать декларативные утверждения в императивном коде
Это позволило бы без дополнительных усилий добавлять в документацию условия контракта.
Это, вероятно, позволило бы посмотреть контракт во время исполнения.

Ну, проверке при компиляции это тоже точно не помешало бы =)

Про specsharp я знаю, но, если я не ошибаюсь, это разработка ms research, не предназначенная для продакшэна. Я был бы очень рад, если бы контракты внедрили в основные языки .net в том виде, в котором они реализованы в спекшарпе.

0
chaliy #
А аха, понял. Да мне тоже не очень нравится текущий синтаксис. Благо что это не более чем метаданные и для языков еще ничего не потерянно ;). В каком нибуть C#5.0 мож и заимплементят ;).

specsharp это первая попытка Майкров написать кодконтракты. Но потом гдето в недрах МС, кто-то приянл решения что это решение должно быть кросс язычное… и… мы получили код контракты в текущем виде. Обае реализации из МС Ресерч. Отличия собно в том что СпекШарп работали с исходниками, а КодКонтракты с ИЛ кодом.
0
mezastel #
Так а даже если бы они только C# использовали, все равно наверное не получилось бы красиво. Контракты — это как мини-доменная область и по-хорошему для нее нужен свой dsl или хотя бы расширения языка чтобы можно было их описывать не отвлекаясь на всякие формальности.
0
paranoik #
я почему-то сразу подумал о постшарпе…
0
mezastel #
а он сейчас между прочем платный. причем автор так хитрит что как бы бесплатные лицензии на 1.5 вдруг раз — и стали платными. не знаю можно ли вообще так делать лицензионно.
+2
GraDea #
Контракты и ассерты имеют совершенно разный смысл. Вся идея контрактов — в гарантиях при взаимодейчтвии различных классов.
Object invariants – для проверки, что данные объекта находятся в хорошем состоянии на протяжении жизни объекта

Это не так. Инварианты действуют при передаче объекта, при его создании. При этом внутри метода вполне возможна «порча» объекта, главное, чтобы при выходе из метода объект был в правильном состоянии.
–6
eisernWolf #
О хосспадзи, я такое самописное юзал у себя в коде еще во второй версии. Однако не делал из этого «что-то». На РСДН-овских форумах, кстати, был еще один толковый человек, с которым мы это обсуждали. Правда, без статического анализа, но это не очень и важно при наличии тестов… Автоматически-сгенерированные тесты это вообще нон-сенс: если у вас достаточно информации, чтобы сгенерировать тест, вам он уже не нужен. И вообще, тесты нужно писать до написания кода, иначе вряд-ли они окажутся действительно полезными. В-общем, в очередной раз убеждаюсь, что своего расцвета дотнеть достигла на второй версии, а потом пошло добавление фич в большинстве случаев необязательных для качественной разработки грамотным инженером. Сейчас вся эта «напичканность» только отпугивает. По сравнению с аскетичным Обджектив-Си просто кошмар.
0
bolzen #
Большое спасибо, очень интересная статья.
Возник вопрос, как правильно поступать с некоторой потерей информативности исключения, ведь
if (orderedItems.Any(p => p == orderItem)) throw new InvalidOperationException(«Item already ordered»);
скажет тому, кто поймает эту ошибку, куда как больше, чем ContractException общего вида, который не дает понять какой именно пункт контракта был нарушен.
Верно ли я понял, что забота о точном исполнении конракта ложится на того, кто использует класс, а для самого класса важно, чтобы он находился в хорошем состоянии и информирование о том, что конкретно было не так при вызове метода, не его обязанность?
0
Regfor #
См. ответ ниже, не туда отписал :)
+1
Regfor #
В таком виде
Contract.Ensures(Contract.Exists(orderedItems, p => p == orderItem), "Item already ordered");
Скажет ровно столько же сколько старый контракт в стиле if-then-throw.

Но на самом деле это скорее дело вкуса. Code contracts обобщенный подход, ну и теряются все преимущества при if-then-throw которые я описал, но если ими можно пожертвовать то можно писать контракты в привычном стиле.

Соблюдать установленный контракт действительно обязанность вызывающей стороны, а вот что конкретно было не так, т.е. нарушение контракта, сообщат сами контракты в классе

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.