Автор: 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, }}
З.Ы. Хотя контракты кажутся полудохлыми, они достаточно интенсивно используются в некоторых проектах внутри Майкрософт. Так, в новой билд-системе они используются на полную. Долгое время даже гоняли статический анализатор, хотя недавно от него все-таки отказались.