Существует одна типичная проблема с проверкой аргументов в конструкторах классов-наследников, которые должны передавать «куски» своих аргументов базовому классу. Простой пример этой проблемы выглядит так:
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;
}
}
З.Ы. Хотя контракты кажутся полудохлыми, они достаточно интенсивно используются в некоторых проектах внутри Майкрософт. Так, в новой билд-системе они используются на полную. Долгое время даже гоняли статический анализатор, хотя недавно от него все-таки отказались.
Здорово, а в доках я это как-то пропустил что ли? Описано это где-нить?
ОтветитьУдалитьПоскольку у нас тут контракты используются по полной, я просто заметил их в декомпилированном коде, который декомпильнул R# при Go to Definition. Я потом специально поискал упоминание этой возможности в официальной доке, но ничего такого не нашел.
УдалитьТак что, если мы и пропустили это в доках, то оба:))
Жаль-то как! Функционал-то очень важный, страшно полезный.
УдалитьДа, мне тоже очень и очень не понятно, почему к контрактам такое отношение.
УдалитьК сожалению, тут и работы слишком много, чтобы сделать что-то подобное в свободное время. Да и в open source в этом случае я не верю: тут нужно хорошее взаимодействие между ресерчерами и разработчиками.
Вот уж действительно "кажутся полудохлыми" - такой важный механизм и так смахивает на заброшенный дом.
ОтветитьУдалитьКонтракты-то теперь open source, но я, чего-то, не верю в то, что такую городушку можно эффективно развивать лишь с помощью комьюнити.
УдалитьИ да, жаль, что этой туле так мало внимания уделяется...
Сергей, по поводу "Да, мне тоже очень и очень не понятно, почему к контрактам такое отношение."
ОтветитьУдалитьМне кажется, ответ вы написали в статье: "магия с контрактами".
"магия" - это значит неоднозначное понимание кода и более высокий уровень входа для новых программистов.
А чтобы избавиться от этой "магии" нужно контракты ввести на уровне синтаксиса языка, как это сделано в Eiffel.
VS 2015 RC совсем не дружит с контрактами
ОтветитьУдалитьпичалька, надеюсь допилят
Да, есть такое дело. Скоро должна эта ситуация исправиться слегка.
Удалить