Гарантируется ли выполнение контрактов кода перед вызовом цепочечных конструкторов?

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

Это проще всего объяснить с помощью (надуманного) примера:

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);
}

который компилируется как

MSIL скомпилированного кода выше

и перемещает вызов в require перед вызовом в цепочечный конструктор:

msil кода выше, переданного через переписчик контракта

Что значит...

Чтобы избежать необходимости прибегать к статическим методам, выполняющим проверку аргументов, используйте переписчик контрактов. Вы можете вызвать переписчик с помощью Contract.Requiresили означая, что блок кода является предварительным условием, заканчивая его Contract.EndContractBlock();, Это приведет к тому, что перезаписывающее устройство поместит его в начало метода перед вызовом инициализатора конструктора.

Другие вопросы по тегам