Автор: Sergey Teplyakov

Существует одна типичная проблема с проверкой аргументов в конструкторах классов-наследников, которые должны передавать куски своих аргументов базовому классу. Простой пример этой проблемы выглядит так:abstract class Base
{
private readonly int _length,
protected Base(int length)
{
if (length <,= 0) throw new ArgumentOutOfRangeException('length'),
_length = length,
}
}
internal class Derived : Base
{
public Derived(string s)
:
base(s.Length)
{
// Проверка бесполезна!
if (string.IsNullOrEmpty(s
)) throw new ArgumentNullException('s'),
}
}

Проблема в том, что конструктор базового класса вызывается до тела метода конструктора Derived (*). В результате, если конструктор наследника принимает составной объект и выкусывает часть, которая нужна конструктору базовому классу, то нормально проверить валидность составного объекта не получиться.

(*) Строго говоря, это не так, и мы посмотрим, как Code Contracts обходят эту проблему.

В результате, приходится вытаскивать валидацию аргументов и выкусывание нужной информации во вспомогательный метод или выкручиваться каким-то другим образом.internal class Derived : Base
{
public Derived(string s)
:
base(ValidateAndExtractLength(s))
{ }
private static int ValidateAndExtractLength(string s)
{
if (string.IsNullOrEmpty(s)) throw new ArgumentNullException('s'),
return s.Length,
}
}

Данный подход работает, но теперь предусловия конструктора (которые являются ключевыми для клиентов класса) размазаны по вспомогательным статическим методам. Альтернатива воспользоваться контрактами (a.k.a. Code Contracts).abstract class Base
{
// дополнительные поля помогут понять, как действует
// компилятор языка C# и Code Contract Rewriter
private readonly int _baseFoo = 42
,
private readonly int _length,
protected Base(int length)
{
Contract.Requires(length >,= 0),
_length = length,
}
}
internal class Derived : Base
{
private readonly string _s,
private readonly int _derivedFoo = 36,
public Derived(string s)
:
base(s.Length)
{
Contract.Requires(!string.IsNullOrEmpty(s)),
_s = s,
}
}

Казалось бы, а в чем разница? Но она есть, и еще какая! Как и в случае с асинхронными методами и блоками итераторов, в случае предусловий в конструкторе, компилятор Code Contracts выполняет более сложное преобразование IL-кода, а не просто заменяет Contract.Requires на генерацию исключения.

ПРИМЕЧАНИЕ
Подробнее о предусловиях асинхронных методов см. Когда предусловия не являются предусловиями.

Приведенный выше код в откомпилированном виде будет выглядеть так:internal abstract class Base_Decompiled
{
private readonly int _baseFoo,
private readonly int _length,
protected Base_Decompiled(int length)
{
// Предусловия базового класса
__ContractsRuntime.Requires(length >,= 0, null, 'length >,= 0'
),
// Инициализатор полей базового класса
this._baseFoo = 42
,
// Вызов конструктора базового класса (System.Object-а)
base..ctor
(),
// Тело конструктора базового класса
this._length = length
,
}
}
internal class Derived_Decompiled : Base_Decompiled
{
private readonly string _s,
private readonly int _derivedFoo,
public Derived_Decompiled(string s)
{
// Предусловия класса-наследника
__ContractsRuntime.Requires
(
!string.IsNullOrEmpty(s), null, '!string.IsNullOrEmpty(s)'),
// Инициализатор полей класса-наследника
this._derivedFoo = 36
,
// Вызов конструктора класса-наследника
base..ctor(s.Length
),
// Тело конструктора класса-наследника
this._s = s,
}
}

Это значит, проверка предусловия класса наследника будет выполнена до вызова конструктор базового класса!

При наличии контрактов процесс конструирования экземпляра наследника будет таким:

  • Проверка предусловий наследника: Contract.Requires(!string.IsNullOrEmpty(s)),
  • Инициализатор полей наследника: _derivedFoo = 36,
  • Вызов конструктора базового класса
    • Проверка предусловия базового класса:
      Contract.Requires(length >,= 0),
    • Инициализатор полей базового класса: _baseFoo = 42,
    • Тело конструктора базового класса: _length = length,
  • Тело конструктора класса-наследника: _s = s,

Существует два важных момента

Во-первых, магия с контрактами работает даже для так называемых Legacy Contract Requires: это значит, что для получения преимуществ контрактов достаточно добавить Contract.EndContractBlock после проверки аргументов.public Derived(string s)
:
base(s.Length)
{
if (string.IsNullOrEmpty(s))
throw new ArgumentNullException('s'),
Contract.EndContractBlock(),
}

А во-вторых, теперь должно быть понятным ограничение контрактов, которое не позволяют использовать поля объекта в предусловиях. Поэтому следующий код не компилируется:internal class Derived : Base
{
// Поле должно быть открытым, чтобы удовлетворять требованию
// доступности предусловий!
public readonly int N = 42
,
private readonly string _s,
public Derived(string s)
:
base(s.Length)
{
Contract.Requires(!string.IsNullOrEmpty(s)),
// error CC1011: This/Me cannot be used in Requires of a constructor
Contract.Requires(N == 42
),
_s = s,
}
}

З.Ы. Хотя контракты кажутся полудохлыми, они достаточно интенсивно используются в некоторых проектах внутри Майкрософт. Так, в новой билд-системе они используются на полную. Долгое время даже гоняли статический анализатор, хотя недавно от него все-таки отказались.

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).