Как заставить инвариантный метод генерировать конкретное исключение в контрактах кода C #?

Я хочу, чтобы мой инвариантный метод объекта генерировал определенное исключение. Имеет ли это смысл? Возможно ли это на C #?

Например, у меня есть следующий код, включая класс A с инвариантным методом и класс исключения E. На данный момент класс E не участвует в A ...

class A {
    int x = 0, y = 1;

    [ContractInvariantMethod]
    private void YisGreaterThanX() {
        Contract.Invariant(x < y);
    }
}

class E : Exception {
}

А мне нужно следующее. Как и Contract.Requires, было бы полезно иметь Contract.Invariant (или может быть конструктор атрибута, который принимает класс, производный от исключения).

class A {
    int x = 0, y = 1;

    [ContractInvariantMethod]
    private void YisGreaterThanX() {
        Contract.Invariant<E>(x < y);
    }
}

class E : Exception {
}

Это хорошее намерение? Может моя логика неверна?


person Hoborg    schedule 22.12.2013    source источник
comment
Contract.Requires может генерировать исключения, чтобы указать на ошибки в коде вызывающего абонента. Если Contract.Invariant выдает ошибку, это почти всегда указывает на ошибку в вашем коде. Можете ли вы подробнее рассказать, почему вы хотите это сделать?   -  person    schedule 22.12.2013
comment
@hvd, благодарю за ответ. Означает ли это, что нужно включать Contract.Requires<E>(x < y) в каждый конструктор типа public A(int x, int y) и все подобные методы? Или это значит, что логика класса А испорчена? ..   -  person Hoborg    schedule 23.12.2013
comment
Да, в таком случае я бы обязательно поместил это в конструктор. Это комбинация недопустимых аргументов, которую лучше всего обозначить ArgumentException, а не экземпляр класса, который затем становится непригодным для использования.   -  person    schedule 23.12.2013
comment
Помещение его в качестве требования к конструктору также помогает с документацией: код, вызывающий конструктор, знает, что x < y является требованием этого конструктора. (По какой-то причине SO не позволил мне отредактировать мой предыдущий комментарий, чтобы включить его, хотя прошло менее пяти минут.)   -  person    schedule 23.12.2013
comment
Очень полезные комментарии. Но ваше предложение влечет за собой дублирование кода (похожее Contract.Requires во всех аналогичных методах), не так ли? Должен ли я написать что-то вроде Contract.Requires<ArgumentException>(Requirement(x, y));, где Requirement определено в классе A как public Func<int, int, bool> Requirement { get { return (x, y) => x < y; } }?   -  person Hoborg    schedule 24.12.2013
comment
Я бы этого не сделал. Статический анализатор этого не поймет, не поможет в документации. Да, может быть небольшое дублирование, но это потому, что две отдельные концепции тесно связаны. Инвариант - это обещание класса своим пользователям. Требования - это ограничения класса для пользователей. Эти ограничения могут потребоваться для поддержания инварианта (x < y в вашем случае, где и x, и y могут быть изменены), но также могут быть ограничения без каких-либо инвариантов или могут быть инварианты, которые не полагаются на какие-либо ограничения, помимо того, что сам C # обеспечивает.   -  person    schedule 25.12.2013
comment
В большинстве случаев, которые я видел, инварианты настолько отличаются от ваших, что дублирование никогда не будет проблемой. Если у вас есть большое количество изменяемых свойств, которые могут нарушать инварианты, я понимаю, что вы бы потратили много времени просто на копирование и вставку. Однако в этом случае я опасаюсь, что у вас могут быть слишком сложные классы. У вас есть конкретный пример, в котором дублирование (по вашему мнению) было бы настолько большим, что затрудняло бы обслуживание вашего кода?   -  person    schedule 25.12.2013
comment
Действительно, у меня был пример. Но после вашего совета могу согласиться, что это чистый пример чрезмерно сложной логики. Таким образом, мне кажется, что я понимаю ваши рассуждения. Спасибо за объяснение.   -  person Hoborg    schedule 25.12.2013


Ответы (1)


Учитывая, что мы не должны обнаруживать сбои контрактов , и поскольку нет перегрузка для Contract.Invariant с явно указанным исключением, этот ответ, надеюсь, является гипотетическим.

Итак, после всех заявлений об отказе от ответственности вы можете взломать что-нибудь вроде следующих, подключив ContractFailed обработчик события:

Contract.ContractFailed += Contract_ContractFailed();

А в обработчике отфильтруйте Invariant отказы, обработайте отказ и повторно вызовите исключение:

public static void Contract_ContractFailed(object sender,  ContractFailedEventArgs e)
{
    if (e.FailureKind == ContractFailureKind.Invariant)
    {
        e.SetHandled();
        throw new E(e.Message, e.OriginalException);
    }
}

Учитывая, что вы не можете передать много информации в определении Contract.Invariant, если вам нужно параметризовать выброшенное исключение, вам нужно будет закодировать предполагаемое исключение, например, в bool, string Contract.Invariant overload или используйте внешнее глобальное состояние, такое как локальное хранилище потока.

ИМО, все это вонючие. Итак, чтобы ответить на ваш последний вопрос, я считаю, что бросать улавливаемые исключения - это вообще не лучшая идея - ваш код вызывается из заданного диапазона состояния, поэтому где-то отсутствует ошибка / проверка.

Редактировать
Впоследствии было замечено, что обработка + бросок в обработчике ContractFailed по-прежнему заключена во внутренний ContractException. Так что вам нужно будет распаковать, например

catch(Exception ex)
{
   var myException = ex.InnerException;
   // ... do something
}
person Community    schedule 23.12.2013
comment
Спасибо за ответ. Например, если бы кто-то использовал предложенную вами технику неприятного запаха, он не смог бы поймать исключение в блоке catch кода try { var a = new A(0, 0); } catch (Exception e) { var x = e.Message; }? - person Hoborg; 24.12.2013
comment
Обрабатывая ContractFailed, устанавливая SetHandled и затем повторно вызывая другое исключение, по сути, это позволяет вам convert Отказ от определенного и улавливаемого исключения, так что да, вы можете затем поместить вокруг этого try / catch. Это неприятно, поскольку сводит на нет цель ContractException как «фатальную» и неуловимую. - person StuartLC; 24.12.2013
comment
Но код try { var a = new A(0, 0); } catch (Exception e) { var x = e.Message; } НЕ перехватывает исключение. Отладчик останавливается на последней скобке метода public static void Contract_ContractFailed(object sender, ContractFailedEventArgs e) в вашем ответе и показывает уведомление о необработанном исключении E. Что мне не хватает? - person Hoborg; 25.12.2013
comment
Может быть, это просто ваши настройки отладки в VS? т.е. он ломается на всех Ex, а не только на необработанном? - person StuartLC; 26.12.2013
comment
Извините, я ошибся со своим недавним комментарием. Этот код работает так, как ожидалось, и настройки отладчика верны. Как-то я подумал, что отладчик не должен запускать помощник по исключениям в случае исключений, которые будут обрабатываться дальше. Ваше предположение помогло мне понять это. Большое спасибо за ответ. - person Hoborg; 26.12.2013