Гарантируется ли выполнение контрактов кода перед вызовом цепочечных конструкторов?
Прежде, чем я начал использовать Контракты Кодекса, я иногда сталкивался с осторожностью, связанной с проверкой параметров при использовании цепочки конструктора.
Это проще всего объяснить с помощью (надуманного) примера:
class Test
{
public Test(int i)
{
if (i == 0)
throw new ArgumentOutOfRangeException("i", i, "i can't be 0");
}
public Test(string s): this(int.Parse(s))
{
if (s == null)
throw new ArgumentNullException("s");
}
}
Я хочу Test(string)
конструктор, чтобы связать Test(int)
конструктор, и для этого я использую int.Parse()
,
Конечно, int.Parse()
не любит иметь нулевой аргумент, поэтому, если s равен нулю, он выдаст, прежде чем я доберусь до строк проверки:
if (s == null)
throw new ArgumentNullException("s");
что делает эту проверку бесполезной.
Как это исправить? Ну, я иногда делал это:
class Test
{
public Test(int i)
{
if (i == 0)
throw new ArgumentOutOfRangeException("i", i, "i can't be 0");
}
public Test(string s): this(convertArg(s))
{
}
static int convertArg(string s)
{
if (s == null)
throw new ArgumentNullException("s");
return int.Parse(s);
}
}
Это немного неудобно, и трассировка стека не идеальна в случае сбоя, но работает.
Теперь вместе с Code Contracts, поэтому я начинаю их использовать:
class Test
{
public Test(int i)
{
Contract.Requires(i != 0);
}
public Test(string s): this(convertArg(s))
{
}
static int convertArg(string s)
{
Contract.Requires(s != null);
return int.Parse(s);
}
}
Все хорошо. Работает нормально. Но потом я обнаруживаю, что могу сделать это:
class Test
{
public Test(int i)
{
Contract.Requires(i != 0);
}
public Test(string s): this(int.Parse(s))
{
// This line is executed before this(int.Parse(s))
Contract.Requires(s != null);
}
}
И тогда, если я сделаю var test = new Test(null)
, Contract.Requires(s != null)
выполняется раньше this(int.Parse(s))
, Это означает, что я могу покончить с convertArg()
тест в целом!
Итак, к моим актуальным вопросам:
- Задокументировано ли это поведение где-нибудь?
- Могу ли я рассчитывать на такое поведение при написании контрактов кода для подобных конструкторов?
- Есть ли какой-то другой способ, которым я должен подходить к этому?
1 ответ
Краткий ответ
Да, поведение задокументировано в определении "предусловия", а также в том, как унаследована проверка (если / то / выбросить) без вызова Contract.EndContractBlock
обрабатывается.
Если вы не хотите использовать Contract.Requires
, вы можете изменить свой конструктор на
public Test(string s): this(int.Parse(s))
{
if (s == null)
throw new ArgumentNullException("s");
Contract.EndContractBlock();
}
Длинный ответ
Когда вы размещаете Contract.*
позвоните в своем коде, вы на самом деле не вызываете члена в System.Diagnostics.Contracts
Пространство имен. Например, Contract.Requires(bool)
определяется как:
[Conditional("CONTRACTS_FULL")]
public static void Requires(bool condition)
{
AssertMustUseRewriter(ContractFailureKind.Precondition, "Requires");
}
AssertMustUseRewriter
безусловно бросает ContractException
, поэтому при отсутствии перезаписи скомпилированного бинарного кода код просто вылетает, если CONTRACTS_FULL
определено. Если оно не определено, предварительное условие никогда даже не проверяется, так как вызов Requires
опускается компилятором C# из-за наличия [Conditional]
атрибут
Переписчик
На основании настроек, выбранных в свойствах проекта, Visual Studio определит CONTRACTS_FULL
и позвонить ccrewrite
генерировать соответствующий IL для проверки контрактов во время выполнения.
Пример контракта:
private string NullCoalesce(string input)
{
Contract.Requires(input != "");
Contract.Ensures(Contract.Result<string>() != null);
if (input == null)
return "";
return input;
}
Составлено с csc program.cs /out:nocontract.dll
, ты получаешь:
private string NullCoalesce(string input)
{
if (input == null)
return "";
return input;
}
Составлено с csc program.cs /define:CONTRACTS_FULL /out:prerewrite.dll
и пробежать ccrewrite -assembly prerewrite.dll -out postrewrite.dll
вы получите код, который фактически выполнит проверку во время выполнения:
private string NullCoalesce(string input)
{
__ContractRuntime.Requires(input != "", null, null);
string result;
if (input == null)
{
result = "";
}
else
{
result = input;
}
__ContractRuntime.Ensures(result != null, null, null);
return input;
}
Главный интерес заключается в том, что наши Ensures
(постусловие) переместился в конец метода, и наши Requires
(предварительное условие) на самом деле не сдвинулось с места, поскольку оно уже было на вершине метода.
Это соответствует определению документации:
[Предварительные условия] - это контракты о состоянии мира при вызове метода.
...
Постусловия - это контракты о состоянии метода, когда он завершается. Другими словами, условие проверяется непосредственно перед выходом из метода.
Теперь сложность вашего сценария заключается в самом определении предварительного условия. Исходя из определения, указанного выше, предварительное условие выполняется до запуска метода. Проблема заключается в том, что спецификация C# говорит, что инициализатор конструктора (цепочечный конструктор) должен вызываться непосредственно перед телом конструктора [CSHARP 10.11.1], что противоречит определению предварительного условия.
Волшебство живет здесь
Код, который ccrewrite
следовательно, генерации не могут быть представлены как C#, так как язык не предоставляет механизма для запуска кода до связанного конструктора (за исключением вызова статических методов в списке параметров связанного конструктора, как вы упомянули). ccrewrite
, как того требует определение, принимает ваш конструктор
public Test(string s)
: this(int.Parse(s))
{
Contract.Requires(s != null);
}
который компилируется как
и перемещает вызов в require перед вызовом в цепочечный конструктор:
Что значит...
Чтобы избежать необходимости прибегать к статическим методам, выполняющим проверку аргументов, используйте переписчик контрактов. Вы можете вызвать переписчик с помощью Contract.Requires
или означая, что блок кода является предварительным условием, заканчивая его Contract.EndContractBlock();
, Это приведет к тому, что перезаписывающее устройство поместит его в начало метода перед вызовом инициализатора конструктора.