Andrey on .NET | Основы Code Contracts

Основы Code Contracts

C# logoCode Contracts – новинка, появившаяся с выходом четвертой версии .NET. Это библиотека, реализующая идею программирования по контракту. Несколько упрощая можно сказать, что её суть заключается в установке условий, которые должны соблюдать параметры методов и свойства объекта.

Основы

Любой разработчик программного обеспечения знает, что доверять полученным значениям никогда нельзя. Например, если в определенный параметр нельзя передавать значение null, то рано или поздно оно там обязательно появится.

Поэтому многие разработчики уделяют много времени созданию различных проверок входящих значений. Их код занимает достаточно объемен, а разработка занимает время. В результате появляются решения вроде вот таких:

public void StoreToDatabase (Customer customer, Item item)
{
    if (customer == null) {
        throw new ArgumentNullException("The customer value cannot be null.");
    }

    if (item == null) {
        throw new ArgumentNullException("The item value cannot be null.");
    }

    if (string.IsNullOrEmpty(customer.Name)) {
        throw new ArgumentNullException("The customer name cannot be null or empty.");
    }

    if (!customer.IsValidItem(item)) {
        throw new ArgumentNullException("This customer cannot order this item.");
    }

    customer.Assign(item);
    this._database.Store(customer);
}  

И это еще не самый худший случай. Согласитесь, что выглядит такой исходный код крайне некрасиво. Да и сопровождать его не очень удобно. На всего лишь две строчки кода пришлось написать двенадцать строк, реализующих проверки входных значений.

В качестве решения может быть предложено использование собственного класса для проверки данных. Это сокращает код и упрощает его чтение. Но тем не менее, данный подход требует создание собственной библиотеки и её сопровождения. Кроме того, её необходимо будет вызывать из каждого метода.

На помощь приходит библиотека Code Contracts. Она создает код, который будет вызываться в начале и конце методов для проверки условий. Кроме того, она позволяет проводить некоторые тесты еще на этапе компиляции. Рассмотрим её использование более подробно.

Устанавливаем Code Contracts

Настройки Code contractsНа самом деле библиотека Code Contracts уже расположена в ядре .NET Framework 4.0. Компилятор также был обновлен для добавления необходимых инструкций в код приложения. Загрузка установочного пакета со страницы DevLabs: Code Contracts  необходима для подключения статических проверок в Microsoft Visual Studio 2010.

После загрузки и установки давайте запустим Visual Studio и создадим пустой консольный проект под названием CodeContractsDemo. После чего откроем его свойства и перейдем на закладку Code Contracts. Здесь доступны настройки, влияющие на компиляцию. Многие из них достаточно понятны. При необходимости можно найти полную документацию на них на сайте Microsoft research.  Давайте включим поддержку контрактов, как показано на приведенном рисунке.

Создаем песочницу для тестов

В созданный проект давайте добавим два класса. Первый из них будем представлять из себя информацию о платеже:

namespace CodeContractsDemo
{
    using System;

    public class Payment
    {
        public string Name { get; set; }
        
        public DateTime Date { get; set; }

        public double Amount { get; set; }
    }
}

Второй, пока пустой, операции с ними:

namespace CodeContractsDemo
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;

    public class PaymentProcessor
    {
    }
}

Обратите внимание на заранее указанное пространство имен для Code Contracts.

Принципы контрактов

Библиотека .NET CodeContracts проповедует принцип разработки по контракту (Design By Contract). В его основе лежит три следующих понятия:

  • Предусловия (precondition) определяют, что должен получить метод при запуске;
  • Постусловия (postcondition) определяют, каким должен быть результат работы;
  • Инварианты (invariant) указывают на объекты, которые не должны изменяться по время работы.

Давайте внимательно разберем каждый из них по отдельности.

Предусловия

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

В Code Contracts для задания предусловий используется метод Contract.Requires(). Давайте добавим в класс PaymentProcessor метод Add() и посмотрим какие условия у него могут быть:

namespace CodeContractsDemo
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;

    public class PaymentProcessor
    {
        private List<Payment> _payments = new List<Payment>();

        public void Add(Payment payment)
        {
            Contract.Requires(payment != null);
            Contract.Requires(!string.IsNullOrEmpty(payment.Name));
            Contract.Requires(payment.Date <= DateTime.Now);
            Contract.Requires(payment.Amount > 0);

            this._payments.Add(payment);
        }
    }
}

Как можно понять из примера, полученное значение не должно быть равно null, как и имя адресата. Кроме того, дата не должна быть из будущего, а сумма платежа должна быть больше нуля.

Конечно, данный вариант можно переписать с использованием if-throw. Но это будет занимать гораздо больше места. Давайте посмотрим на еще один пример (добавим метод также в класс PaymentProcessor):

public void Add2(Payment[] payments)
{
    Contract.Requires(
        (payments != null) &&
        Contract.ForAll(payments, payment => payment != null));

    foreach (var payment in payments) {
        this._payments.Add(payment);                
    }
}

Контракт Contract.ForAll(), проверяет все значения перечисления на соответствие заданному в лямбда-выражении условию. Для реализации его аналога потребуется уже применение цикла foreach, что явно увеличит объем кода.

Давайте скомпилируем проект и перейдем к методу Main. В нем разместим в нем следующий код:

namespace CodeContractsDemo
{
    using System;
    using System.Collections.Generic;

    class Program
    {
        static void Main(string[] args)
        {
            var paymentProcessor = new PaymentProcessor();
            paymentProcessor.Add(null);

            var payments = new List<Payment>() {
                new Payment(),
                new Payment()
            };

            payments.Add(null);

            paymentProcessor.Add2(payments.ToArray());

            Console.ReadKey(true);
        }
    }
}

Если теперь запустить проект, то в строке paymentProcessor.Add(null); будет выброс исключения. Можно её закомментировать. Но теперь исключение будет выброшено при вызове метода Add2(). Ведь и в том и в другом случае не соблюдалось предусловие. Для работы приложения, необходимо передать в качестве параметров корректные выражения.

Постусловия

Как правило, при работе метода необходимо убедиться, что и полученный от него результат будет соответствовать определенным правилам. Их и будем называть постусловиями. Для их указания в Code Contracts используется метод Contract.Ensures().

Здесь раскрывается еще одно отличие Code Contracts от реализации при помощи блока if-throw. Дело в том, что вызовы Contract.Ensures() не обязательно размещать в конце метода. Все условия можно расположить в его начале.

Не будем откладывать и сразу посмотрим на пример постусловия. По прежнему в классе PaymentProcessor создадим метод GetPaymentsTotal(), который возвращает сумму всех платежей сделанных от указанного имени name.

public double GetPaymentsTotal(string name)
{
    Contract.Requires(!string.IsNullOrWhiteSpace(name));

    Contract.Ensures(Contract.Result<double>() >= 0);

    double total = 0.0;

    foreach (var payment in this._payments) {
        if (string.Equals(payment.Name, name)) {
            total += payment.Amount;
        }
    }

    return total;
}

В данном случае, что бы не случилось при работе метода GetPaymentsTotal(), при его завершении должно выполняться одно условие: результат его работы всегда должен быть больше нуля. Кроме того, обратите внимание на еще одно отличие от блоков if-throw. Указать значение, являющееся результатом метода, можно вызовом Contract.Result<T>(). Таким образом, оно будет проверено не зависимо от того из какой переменной метода оно получено. И наоборот, при использовании if-throw пришлось бы явно указывать переменную, а сам блок размещать перед оператором return.

Инварианты

Рассмотрим последний из перечисленных основных понятий Code Contracts – инварианты. Это условия, которые должны выполняться на протяжении всего времени существования объекта. Для их указания необходимо в исходном коде класса создать метод, отмеченный атрибутом [ContractInvariantMethod]. Его тело может содержать только вызовы Contract.Invariant() в которых определяются необходимые условия.

В качестве примера создадим класс Point, предназначенный хранения координат точки. При этом условимся, что значения X и Y всегда должны быть положительные.

namespace CodeContractsDemo
{
    using System;
    using System.Diagnostics.Contracts;

    public class Point
    {
        public int X { get; set; }
        public int Y { get; set; }

        public Point()
        {
        }

        public Point(int x, int y)
        {
            this.X = x;
            this.Y = y;
        }

        public void Set(int x, int y)
        {
            this.X = x;
            this.Y = y;
        }

        public void Test(int x, int y)
        {
            for (int dx = -x; dx <= x; dx++) {
                this.X = dx;
                Console.WriteLine("Current X = {0}", this.X);
            }

            for (int dy = -y; dy <= y; dy++) {
                this.Y = dy;
                Console.WriteLine("Current Y = {0}", this.Y);
            }

            Console.WriteLine("X = {0}", this.X);
            Console.WriteLine("Y = {0}", this.Y);
        }

        [ContractInvariantMethod]
        private void ValidateCoordinates()
        {
            Contract.Invariant(this.X >= 0);
            Contract.Invariant(this.Y >= 0);
        }
    }
}

Метод ValidateCoordinates() определяет указанные выше условия и объединяет их в одном месте.

При использовании класса Point значения X и Y можно устанавливать по разному. Но после вызова любого метода класса или обращения к свойствам будет происходить проверка значений координат, а в случае её провала – выброс исключения. Таким образом, можно быть уверенным, что X и Y будут больше или равны нулю на протяжении всего времени существования любого экземпляра класса Point.

Обратите внимание, что внутри тела метода условия не проверяются. Это наглядно демонстрирует вызов Test(). Как можно понять из исходного кода, в процессе его работы X и Y могут временно принимать значения меньше нуля. Однако, выброс исключения будет только в случае, если они такими и останутся.

Изменить описанное поведение можно объявив Test() как "чистый". Т.е. не изменяющий в процессе свой работы состояние объекта. Для этого служит атрибут [Pure]:

[Pure]
public void Test(int x, int y)
{
    .........
}

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

Статическая проверка (Static Checker)

Code Contracts не только следит за выполнением условий в процессе работы приложения. Он также может выявлять потенциальные проблемы еще на этапе его компиляции. Это статическая проверка, которая доступна только в Visual Studio 2010 Premium и Ultimate Edition.

Например, посмотрим на сообщения, выводимые при сборке демонстрационного проекта. Среди них можно найти рекомендации, какие контакты следует установить для того или иного метода. Например:

CodeContractsDemo\PaymentProcessor.cs(18,13): message : 
CodeContracts: Suggested requires: Contract.Requires(this._payments != null);

Более того, Code Contracts может проверять некоторые условия также на этапе компиляции. При этом он указывает места, где вероятнее всего будут выброшены исключения. Например:

CodeContractsDemo\PaymentProcessor.cs(47,13): warning : 
CodeContracts: ensures unproven: Contract.Result<double>() >= 0

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

Кроме того, в рассмотренном выше методе Test() будут отмечены присвоения значений свойствам X и Y как места, в которых возможно не соблюдение контракта. При этом наличие [Pure] роли не играет.

Также стоит заметить, что указанные строки выделяются подчеркиванием в редакторе Visual Studio.

При необходимости, например для увеличения производительности приложения, можно оставить только статическую проверку. Для этого в параметрах Code Contracts необходимо установить Perfom Runtime Contract Checking в значение none.

Во второй части продолжим рассмотрение Code Contracts: посмотрим как указать контракт для интерфейса и реагировать на ошибки. А также будет приведена ссылка на исходный код примеров.

Комментарии (13) -

Максим 30.06.2011 14:34:55

Отличная статья, Андрей!
Возник вопрос. Конструкция if-throw позволяет нам контролировать текст сообщения об ошибке, его локализацию, логгирование ошибки. Есть ли у контрактов возможности для этих действий?

@ Максим: Хороший вопрос. Решил добавить в статью. Но раз уже текста многовато, то сделал как отдельную часть.

Contract.Requires(payment.Name != null); ошибка исправьте.

@ Юрий: Спасибо за внимательность.

Если теперь запустить проект, то в строке paymentProcessor.Add(null); будет выброс исключения.

Нет выброса исключения. Почему? Изменил код :

try
            { paymentProcessor.Add(null); }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }

Нет исключения.

@ R1: У вас похоже выключены Code Contracts (я описывал их настроку).

Александр 18.04.2012 2:15:58

@ R1: Использовать контракты можно выставив символ условной компиляции

CONTRACTS_FULL
Но тогда не будет возможности использовать статическую проверку.

DmitryBLR 25.01.2013 0:42:15



    Contract.Requires(name != null);
    Contract.Requires(!string.IsNullOrWhiteSpace(name));


Проверяете на null, а в следующей вновь также на null

@ DmitryBLR: Спасибо за наблюдательность.

Максим 19.10.2013 17:16:44

Возможно ли использовать code contracts на продакшене?

@ Максим: Вопрос производительности. А это уже надо мерить для конкретного приложения.

Максим 22.10.2013 15:08:24

Не совсем понимаю, почему CodeContracts начинают работать только после установки дополнения к visual studio отсюда visualstudiogallery.msdn.microsoft.com/1ec7db13-3363-46c9-851f-1ce455f66970 В смысле: зачем нужно было это делать отдельным инсталлером?

Как вариант - чтобы CodeContracts могли развиваться независимо от VS (не ждать очередного update или выхода новой версии).

Добавить комментарий