Контракты кода

71

Проектирование по контракту — идея, позаимствованная из языка программирования Eiffel. В пространство имен System.Diagnostics.Contracts версии .NET 4 включены классы для статических проверок кода и проверок времени выполнения, которые могут применяться всеми языками .NET.

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

Информация о контракте может быть скомпилирована в отладочный и рабочий код. Можно также определить отдельную сборку контракта, и многие проверки также могут быть выполнены статически, без запуска приложения.

Контракты допускается определять на интерфейсах; это вынудит реализации интерфейсов удовлетворять требования этих контрактов. Инструменты для работы с контрактами могут переписать сборку, внедрив проверки контракта в код для их выполнения во время функционирования приложения, проверять контракты во время компиляции и добавлять информацию о контрактах в генерируемую документацию XML.

На рисунке ниже показано окно свойств проекта с параметрами для контрактов кода в Visual Studio 2010. Здесь можно определить уровень необходимых проверок времени выполнения, указать необходимость открытия диалогов с предупреждениями в случае нарушения контракта, а также сконфигурировать статические проверки. Установка Perform Runtime Contract Checking (Проводить проверку контракта во время выполнения) в Full (Полная) приводит к определению символа CONTRACTS_FULL. Поскольку многие из методов контрактов аннотированы атрибутом [Conditional ("CONTRACT_FULL") ], все проверки времени выполнения осуществляются только при этой установке:

Контракты кода

Для работы с контрактами кода можно использовать классы, доступные в пространстве имен System.Diagnostics.Contracts в .NET 4. Однако в Visual Studio 2010 соответствующий инструмент отсутствует. Понадобится загрузить расширение для Visual Studio из сайта Microsoft DevLabs. Для проведения статического анализа с помощью этого инструмента требуется версия Visual Studio Team System, а для анализа времени выполнения достаточно версии Visual Studio Standard Edition.

Контракты кода определяются классом Contract. Все требования контрактов, которые задаются в методе, независимо от того, являются они предусловиями или постусловиями, должны помещаться в начало метода. Можно также назначить глобальный обработчик события ContractFailed, который будет вызываться при каждом нарушении контракта во время выполнения. Вызов SetHandled() с параметром е типа ContractFailedEventArgs прекратит стандартное поведение при сбоях с генерацией исключения.

Contract.ContractFailed += (sender, e) =>
   {
        Console.WriteLine(e.Message);
        e.SetHandled();
   };

Предусловия (Requires)

Предусловия проверяют параметры, переданные в метод Requires() и Requires<TException>() — это предусловия, которые могут быть определены с помощью класса Contract. Методу Requires() должно передаваться булевское значение, а также необязательная строка сообщения во втором параметре, которая отображается в случае, если условие не выполнено. В следующем примере требуется, чтобы значение аргумента min было меньше или равно значению аргумента max:

static void MinMax(int min, int max)
{
     Contract.Requires(min <= max);
     // ...
}

Показанный ниже контракт генерирует исключение ArgumentNullException, если аргумент o равен null. Исключение не генерируется, если обработчик события ContractFailed устанавливает событие в handled (обработано). Кроме того, если в окне свойств проекта флажок Assert on Contract Failure (Утверждение при нарушении контракта) отмечен, то вместо генерации определенного исключения вызывается метод Trace.Assert() для прекращения работы программы:

static void Preconditions(object o)
{
    Contract.Requires(o != null,
        "Передаваемый объект в методе Preconditions имеет значение null!");
    // ...
}

Вызов Requires<TException>() не аннотирован атрибутом [Conditional ("CONTRACTS_FULL")], и также не имеет условия для символа DEBUG, поэтому данная проверка времени выполнения осуществляется в любом случае. Requires<TException>() генерирует определенное исключение, если условие не выполнено.

В огромной массе унаследованного кода аргументы часто проверяются операторами if с генерацией исключения в случае нарушения заданного условия. Благодаря контрактам кода, нет необходимости переписывать верификацию. Достаточно лишь добавить одну строку кода:

if (o == null) 
   throw new ArgumentNullException("o", 
       "Передаваемый объект в методе Preconditions имеет значение null!");
Contract.EndContractBlock();

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

Для проверки коллекций, которые используются в качестве аргументов, в классе Contract предусмотрены методы Exists() и ForAll(). Метод ForAll() проверяет каждый элемент в коллекции на предмет соответствия условию. В следующем примере производится проверка, что каждый элемент коллекции имеет значение меньше 12. С помощью метода Exists() можно проверить, соответствует ли условию хотя бы один элемент коллекции:

static void ArrayTest(int[] data)
{
    Contract.Requires(Contract.ForAll(data, i => i < 12));
}

Оба метода, Exists() и ForAll(), имеют перегрузку, которой вместо IEnumerable<T> можно передать два целых числа — fromInclusive и toExclusive. Диапазон чисел (исключая toExclusive) передается делегату Predicate<int>, определенному в третьем параметре. Exists() и ForAll() могут использоваться с предусловиями, постусловиями, а также инвариантами.

Постусловия (Ensures)

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

Ensures() и EnsuresOnThrow<TException>() — это постусловия. Следующий контракт гарантирует, что значение переменной sharedState будет меньше 6 в конце метода. До этого значение может изменяться:

private static int shardState = 5;
static void Postconditions()
{
    Contract.Ensures(shardState < 6);
    shardState = 9;
    Console.WriteLine("shardState = " + shardState);
    shardState = 3;
    Console.WriteLine("shardState = " + shardState);
}

С помощью EnsuresOnThrow<TException>() гарантируется, что разделенное состояние удовлетворит условию в случае генерации указанного исключения.

Для гарантирования возвращаемого значения с контрактом Ensures() может использоваться специальное значение Result<T>. Здесь результат типа int также определен обобщенным типом T для метода Result(). Контракт Ensures() гарантирует, что возвращаемое значение будет меньше 6.

static int ReturnValue()
{
    Contract.Ensures(Contract.Result<int>() < 6);
    return 3;	,
}

Можно также сравнивать значение со старым значением. Это делается с помощью метода 01dValue<T>(), который возвращает исходное значение передаваемой методу переменной.

Инварианты

Инварианты определяют контракты для переменных на протяжении времени жизни метода. Contract.Requires() определяет входные требования, Contract.Ensures() — требования на конец метода. Contract.Invariant() определяет условия, которые должны соблюдаться на протяжении времени жизни метода:

static void Invariant(ref int x)
{
    Contract.Invariant(x > 5);
    x = 3;
    Console.WriteLine("значение инварианта: {0}", x);
    x = 9;
}
Пройди тесты
Лучший чат для C# программистов