Наличие флага, указывающего, должны ли мы выдавать ошибки

64

Я недавно начал работать в месте с некоторыми намного более старшими разработчиками (приблизительно 50 лет). Они работали над критическими приложениями, касающимися авиации, где система не могла выйти из строя. В результате старший программист имеет тенденцию кодировать этот путь.

Он имеет тенденцию помещать логическое значение в объекты, чтобы указать, должно ли быть выброшено исключение.

пример

public class AreaCalculator
{
    AreaCalculator(bool shouldThrowExceptions) { ... }
    CalculateArea(int x, int y)
    {
        if(x < 0 || y < 0)
        {
            if(shouldThrowExceptions) 
                throwException;
            else
                return 0;
        }
    }
}

(В нашем проекте метод может потерпеть неудачу, потому что мы пытаемся использовать сетевое устройство, которое не может присутствовать в данный момент. Пример области является просто примером флага исключения)

Мне это кажется запахом кода. Написание модульных тестов становится немного сложнее, так как вам приходится каждый раз проверять наличие флага исключения. Кроме того, если что-то пойдет не так, разве вы не хотите знать сразу? Разве это не обязанность звонящего определить, как продолжить?

Его логика / рассуждение заключается в том, что наша программа должна сделать 1 вещь, показать данные пользователю. Любое другое исключение, которое не мешает нам делать это, должно игнорироваться. Я согласен, что их не следует игнорировать, но они должны всплывать и обрабатываться соответствующим человеком, и для этого не нужно разбираться с флагами.

Это хороший способ обработки исключений?

Редактировать : просто, чтобы дать больше контекста по проекту решения, я подозреваю, что это потому, что, если этот компонент выходит из строя, программа все равно может работать и выполнять свою основную задачу. Таким образом, мы бы не хотели генерировать исключение (и не обрабатывать его?), Чтобы оно закрывало программу, когда для пользователя она работает нормально.

Редактировать 2 : Чтобы дать еще больше контекста, в нашем случае вызывается метод для сброса сетевой карты. Проблема возникает, когда сетевая карта отключена и снова подключена, ей назначается другой IP-адрес, поэтому Reset выдаст исключение, потому что мы будем пытаться сбросить аппаратное обеспечение со старым IP-адресом.

никола
источник
22
C # имеет соглашение для этого шаблона Try-Parse. дополнительная информация: docs.microsoft.com/en-us/dotnet/standard/design-guidelines/… Флаг не соответствует этому шаблону.
Питер
18
Это в основном управляющий параметр, и он меняет способ выполнения внутренних методов. Это плохо, независимо от сценария. martinfowler.com/bliki/FlagArgument.html , softwareengineering.stackexchange.com/questions/147977/… , medium.com/@amlcurran/…
Бик
1
В дополнение к комментарию Try-Parse от Питера, есть хорошая статья об исключениях Vexing
Linaith
2
«Таким образом, мы бы не хотели генерировать исключение (и не обрабатывать его?) И заставлять его закрывать программу, когда для пользователя все работает нормально» - вы знаете, вы можете ловить исключения, верно?
user253751
1
Я почти уверен, что это уже было где-то рассмотрено, но, учитывая простой пример области, я был бы более склонен задаться вопросом, откуда будут поступать эти отрицательные числа и сможете ли вы обработать это условие ошибки где-то еще (например, что бы ни читал файл, содержащий длину и ширину, например); Тем не менее, «мы пытаемся использовать сетевое устройство, которое не может присутствовать в данный момент». Дело может быть в совершенно другом ответе, это сторонний API или отраслевой стандарт, такой как TCP / UDP?
JRH

Ответы:

73

Проблема с этим подходом состоит в том, что, хотя исключения никогда не генерируются (и, следовательно, приложение никогда не падает из-за необработанных исключений), возвращаемые результаты не обязательно являются правильными, и пользователь может никогда не знать, что существует проблема с данными (или что это за проблема и как ее исправить).

Чтобы результаты были правильными и значимыми, вызывающий метод должен проверить результат на наличие специальных чисел, т. Е. Конкретных возвращаемых значений, используемых для обозначения проблем, возникших при выполнении метода. Отрицательные (или нулевые) числа, возвращаемые для положительно определенных величин (например, площадь), являются ярким примером этого в старом коде. Если вызывающий метод не знает (или забывает!) Проверять наличие этих специальных номеров, обработка может продолжаться, даже не обнаружив ошибки. Затем данные отображаются для пользователя, показывая область 0, которая, как он знает, является неверной, но в них нет указания, что пошло не так, где и почему. Затем они задаются вопросом, являются ли какие-либо другие значения неправильными ...

Если возникнет исключение, обработка будет остановлена, ошибка (в идеале) будет зарегистрирована, и пользователь может быть уведомлен каким-либо образом. Затем пользователь может исправить все, что не так, и повторить попытку. Правильная обработка исключений (и тестирование!) Гарантирует, что критические приложения не будут аварийно завершаться или иным образом оказываться в недопустимом состоянии.

mmathis
источник
1
@Quirk Впечатляет, как Чену удалось нарушить Принцип Единой Ответственности всего за 3 или 4 строки. Это настоящая проблема. Кроме того, проблема, о которой он говорит (программист не задумывается о последствиях ошибок в каждой строке), всегда возможна с непроверенными исключениями и только иногда возможна с проверенными исключениями. Я думаю, что видел все аргументы против проверенных исключений, и ни один из них не является действительным.
Нет U
@TKK лично я столкнулся с некоторыми случаями, когда мне действительно понравились бы проверенные исключения в .NET. Было бы неплохо, если бы были некоторые инструменты статического анализа высокого класса, которые могли бы гарантировать, что API документирует в качестве своих сгенерированных исключений, хотя это, вероятно, было бы практически невозможно, особенно при доступе к собственным ресурсам.
JRH
1
@jrh Да, было бы неплохо, если бы что-то добавляло безопасность исключений в .NET, подобно тому, как TypeScript кладет безопасность безопасности в JS.
Нет U
47

Это хороший способ обработки исключений?

Нет, я думаю, что это довольно плохая практика. Создание исключения по сравнению с возвратом значения является фундаментальным изменением в API, изменением сигнатуры метода и заставлением метода вести себя совершенно по-другому с точки зрения интерфейса.

В общем, когда мы разрабатываем классы и их API, мы должны учитывать, что

  1. может быть несколько экземпляров класса с разными конфигурациями, плавающими в одной и той же программе в одно и то же время, и,

  2. из-за внедрения зависимостей и любого другого числа методов программирования один клиент-потребитель может создавать объекты и передавать их другим, используя их - так часто мы имеем разделение между создателями объектов и пользователями объектов.

Теперь рассмотрим, что должен делать вызывающий метод, чтобы использовать экземпляр, который ему / ей был передан, например, для вызова метода вычисления: вызывающий должен будет проверить как нулевую область, так и исключение перехвата - ой! Вопросы тестирования касаются не только самого класса, но и обработки ошибок вызывающих ...

Мы всегда должны делать вещи максимально легкими для потребителя; эта логическая конфигурация в конструкторе, которая изменяет API метода экземпляра, является противоположностью того, чтобы потребляющий клиентский программист (возможно, вы или ваш коллега) попал в пропасть успеха.

Чтобы предложить оба API, вам гораздо лучше и более нормально либо предоставить два разных класса - один, который всегда выдает ошибку, и другой, который всегда возвращает 0 при ошибке, или предоставить два разных метода с одним классом. Таким образом, потребительский клиент может легко точно знать, как проверять и обрабатывать ошибки.

Используя два разных класса или два разных метода, вы можете гораздо проще использовать IDE для поиска пользователей методов и функций рефакторинга и т. Д., Т. К. Эти два варианта использования более не взаимосвязаны. Чтение, написание, сопровождение, обзоры и тестирование кода также проще.


С другой стороны, я лично считаю, что мы не должны принимать булевы параметры конфигурации, когда фактические вызывающие просто передают константу . Такая параметризация конфигурации объединяет два отдельных варианта использования без реальной выгоды.

Взгляните на свою базу кода и посмотрите, используется ли когда-либо переменная (или неконстантное выражение) для логического параметра конфигурации в конструкторе! Я сомневаюсь.


И дальнейшее рассмотрение состоит в том, чтобы спросить, почему вычисление области может потерпеть неудачу. Лучше всего добавить конструктор, если вычисление не может быть выполнено. Однако, если вы не знаете, можно ли выполнить вычисление до дальнейшей инициализации объекта, возможно, стоит рассмотреть возможность использования различных классов для различения этих состояний (не готовых для вычисления площади и готовой для вычисления области).

Я читал, что ваша ситуация неудачи ориентирована на удаленное взаимодействие, поэтому может не применяться; просто пища для размышлений.


Разве это не обязанность звонящего определить, как продолжить?

Да, я согласен. Вызывающему абоненту кажется преждевременным решать, что область 0 является правильным ответом в условиях ошибки (особенно, если 0 является допустимой областью, поэтому невозможно определить разницу между ошибкой и фактическим 0, хотя это может не относиться к вашему приложению).

Эрик Эйдт
источник
Вам вообще не нужно проверять исключение, потому что вы должны проверять аргументы перед вызовом метода. Сравнение результата с нулем не различает допустимые аргументы 0, 0 и недопустимые отрицательные. API действительно ужасный ИМХО.
Блэкджек
Приложения K MS в приложении для iostream C99 и C ++ являются примерами API, где ловушка или флаг радикально изменяют реакцию на сбои.
дедупликатор
37

Они работали над критическими приложениями, касающимися авиации, где система не могла выйти из строя. В следствии ...

Это интересное введение, которое создает у меня впечатление, что мотивация этого дизайна состоит в том, чтобы избегать исключения в некоторых контекстах «потому что тогда система может выйти из строя» . Но если система «может выйти из строя из-за исключения», это явный признак того, что

  • исключения не обрабатываются должным образом , по крайней мере, жестко.

Так что, если программа, использующая метод, AreaCalculatorсодержит ошибки, вы, коллега, предпочитаете, чтобы программа не «рано работала при сбое», а возвращала какое-то неправильное значение (надеясь, что никто не заметит это, или надеясь, что никто не сделает с ним что-то важное). Это на самом деле маскировка ошибки , и, по моему опыту, она рано или поздно приведет к последующим ошибкам, для которых становится трудно найти основную причину.

ИМХО, написание программы, которая не дает сбоя ни при каких обстоятельствах, но показывает неверные данные или результаты вычислений, обычно ничем не лучше, чем дать сбой программе. Единственный правильный подход - дать возможность вызывающему абоненту заметить ошибку, разобраться с ней, позволить ему решить, должен ли пользователь быть информирован о неправильном поведении и безопасно ли продолжать какую-либо обработку или безопаснее полностью остановить программу. Таким образом, я бы порекомендовал одно из следующего:

  • затрудняет упускание из виду того факта, что функция может выдать исключение. Документация и стандарты кодирования здесь ваши друзья, и регулярные обзоры кода должны поддерживать правильное использование компонентов и правильную обработку исключений.

  • научить команду ожидать и иметь дело с исключениями, когда они используют компоненты «черного ящика», и учитывать глобальное поведение программы.

  • если по каким-то причинам вы думаете, что не можете заставить вызывающий код (или разработчиков, которые его пишут) правильно использовать обработку исключений, то в качестве последнего средства вы можете разработать API с явными переменными вывода ошибок и без исключений вообще, например

    CalculateArea(int x, int y, out ErrorCode err)

    так что вызывающему очень трудно игнорировать функцию, которая может потерпеть неудачу. Но это ИМХО крайне некрасиво в C #; это старый метод защитного программирования из C, в котором нет исключений, и обычно не должно быть необходимости работать в наше время.

Док Браун
источник
3
«Написание программы, которая не дает сбоя ни при каких обстоятельствах, но показывает неверные данные или результаты расчетов, обычно ничем не лучше, чем допускать сбой программы». Я полностью согласен в целом, хотя я мог представить, что в авиации я бы предпочел иметь самолет продолжая работать с приборами, показывающими неверные значения по сравнению с выключением компьютера самолета. Для всех менее важных приложений определенно лучше не маскировать ошибки.
Триларион
18
@Trilarion: если программа для бортового компьютера не содержит надлежащей обработки исключений, «исправить это», заставив компоненты не генерировать исключения, является очень ошибочным подходом. Если программа дает сбой, должна быть какая-то резервная система резервного копирования, которая может вступить во владение. Например, если программа не дает сбоя и показывает неправильную высоту, пилоты могут подумать, что «все в порядке», пока самолет летит в следующую гору.
Док Браун
7
@Trilarion: если бортовой компьютер показывает неправильную высоту, и самолет из-за этого рухнет, это тоже вам не поможет (особенно, если система резервного копирования находится там и не получает информацию о том, что она должна вступить во владение). Системы резервного копирования для бортовых компьютеров - не новая идея, Google для «систем резервного копирования бортовых компьютеров», я уверен, что инженеры во всем мире всегда встраивали избыточные системы в любую реальную жизненно важную систему (и если бы это было не из-за потери страхование).
Док Браун
4
Этот. Если вы не можете позволить себе потерпеть крах программы, вы также не можете позволить ей молча давать неправильные ответы. Правильный ответ - иметь соответствующую обработку исключений во всех случаях. Для веб-сайта это означает глобальный обработчик, который преобразует непредвиденные ошибки в 500. У вас также могут быть дополнительные обработчики для более специфических ситуаций, таких как наличие try/ catchвнутри цикла, если вам требуется обработка для продолжения в случае сбоя одного элемента.
jpmc26
2
Получение неправильного результата всегда является худшим видом неудачи; это напоминает мне правило об оптимизации: «сделайте это правильно, прежде чем оптимизировать, потому что получение неправильного ответа быстрее все равно никому не выгодно».
Тоби Спейт
13

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

Любую функцию с n параметрами будет сложнее проверить, чем с n-1 параметрами. Распространяйте это до абсурда, и становится аргумент, что функции не должны иметь параметров вообще, потому что это облегчает их тестирование.

Хотя это хорошая идея написать код, который легко тестировать, ужасная идея поставить простоту тестирования выше написания кода, полезного для людей, которые должны его вызывать. Если в примере в вопросе есть переключатель, который определяет, будет ли выброшено исключение, возможно, что вызывающие номера, желающие такого поведения, заслуживают добавления его в функцию. Где грань между сложным и слишком сложным лежит - это суждение; Любой, кто пытается сказать вам, что есть яркая линия, которая применяется во всех ситуациях, должен с подозрением смотреть на это.

Кроме того, если что-то пойдет не так, разве вы не хотите знать сразу?

Это зависит от вашего определения неправильно. Пример в вопросе определяет неверно как «дано измерение меньше нуля и shouldThrowExceptionsверно». Если задано измерение, если оно меньше нуля, это не так, когда shouldThrowExceptionsложно, потому что переключатель вызывает другое поведение. Это просто не исключительная ситуация.

Настоящая проблема здесь заключается в том, что переключатель был назван плохо, потому что он не описывает, что делает функция. Если бы ему дали лучшее имя, как treatInvalidDimensionsAsZero, вы бы задали этот вопрос?

Разве это не обязанность звонящего определить, как продолжить?

Вызывающий действительно определяет, как продолжить. В этом случае он делает это заранее, устанавливая или сбрасывая, shouldThrowExceptionsи функция ведет себя в соответствии со своим состоянием.

Пример патологически прост, потому что он выполняет один расчет и возвращает результат. Если вы сделаете это немного более сложным, например, вычислите сумму квадратных корней списка чисел, бросание исключений может вызвать проблемы у вызывающих абонентов, которые они не могут решить. Если я передаю список [5, 6, -1, 8, 12]и функция выдает исключение поверх -1, я не могу сказать, чтобы функция продолжала работать, потому что она уже прервала и выбросила сумму. Если список представляет собой огромный набор данных, создание копии без каких-либо отрицательных чисел перед вызовом функции может быть нецелесообразным, поэтому я вынужден заранее сказать, как следует обрабатывать недопустимые числа, либо в форме «просто игнорируйте их» "Переключить или, может быть, предоставить лямбда, которая вызывается для принятия этого решения.

Его логика / рассуждение заключается в том, что наша программа должна сделать 1 вещь, показать данные пользователю. Любое другое исключение, которое не мешает нам делать это, должно игнорироваться. Я согласен, что их не следует игнорировать, но они должны всплывать и обрабатываться соответствующим человеком, и для этого не нужно разбираться с флагами.

Опять же, не существует единого решения для всех. В этом примере, по-видимому, функция была написана для спецификации, в которой говорится, как обращаться с отрицательными измерениями. Последнее, что вы хотите сделать, это понизить отношение сигнал / шум в ваших журналах, заполнив их сообщениями, которые говорят: «Обычно здесь возникает исключение, но вызывающая сторона сказала, что не стоит беспокоиться».

И как один из тех гораздо более старых программистов, я бы попросил вас, пожалуйста, отойти от моего газона. ;-)

Blrfl
источник
Я согласен с тем, что наименование и намерение чрезвычайно важны, и в этом случае правильное имя параметра действительно может перевернуть таблицы, поэтому +1, но 1. our program needs to do 1 thing, show data to user. Any other exception that doesn't stop us from doing so should be ignoredмышление может привести к тому, что пользователь примет решение на основе неверных данных (потому что на самом деле программа должна сделать 1 вещь - чтобы помочь пользователю в принятии обоснованных решений) 2. Подобные случаи, как bool ExecuteJob(bool throwOnError = false)правило, чрезвычайно подвержены ошибкам и приводят к коду, о котором трудно думать, просто читая его.
Евгений Подскал
@EugenePodskal Я полагаю, что «показать данные» означает «показать правильные данные». Спрашивающий не говорит, что готовый продукт не работает, просто он может быть написан «неправильно». Я должен увидеть некоторые точные данные по второму пункту. У меня есть несколько часто используемых функций в моем текущем проекте, в которых есть переключатель throw / no-throw, и об этом нет ничего сложнее, чем любая другая функция, но это одна точка данных.
Blrfl
Хороший ответ, я думаю, что эта логика применима к гораздо более широкому кругу обстоятельств, чем просто ОП. Кстати, новый cpp имеет версию throw / no-throw именно по этим причинам. Я знаю, что есть небольшие отличия, но ...
drjpizzle
8

Код безопасности и «нормальный» код могут привести к совершенно разным представлениям о том, как выглядит «хорошая практика». Есть много совпадений - некоторые вещи являются рискованными и их следует избегать в обоих - но все еще есть существенные различия. Если вы добавите требование, чтобы быть гарантированно отзывчивым, эти отклонения становятся весьма существенными.

Они часто относятся к вещам, которые вы ожидаете:

  • Для git неправильный ответ может быть очень плохим по отношению к: взятие на длинное / прерывание / зависание или даже сбой (что, по сути, не является проблемой, скажем, случайного изменения зарегистрированного кода).

    Тем не менее: Для приборной панели, имеющей задержку вычисления g-силы и препятствующей выполнению расчета скорости воздушного потока, может быть неприемлемо.

Некоторые менее очевидны:

  • Если вы опробовали много , результаты первого порядка (например , правильные ответы) не столь большой , беспокойство , условно говоря. Вы знаете, что ваше тестирование охватит это. Однако, если было скрытое состояние или поток управления, вы не знаете, что это не будет причиной чего-то более тонкого. Это трудно исключить при тестировании.

  • Быть явно безопасным относительно важно. Не многие покупатели будут сомневаться в том, является ли покупаемый ими источник безопасным или нет. Если вы находитесь на рынке авиации с другой стороны ...

Как это относится к вашему примеру:

Я не знаю. Существует ряд мыслительных процессов, которые могут иметь ведущие правила, такие как «Никто не бросает в производственном коде», принятый в критически важном для безопасности коде, что было бы довольно глупо в более обычных ситуациях.

Некоторые из них касаются встраивания, некоторые - безопасность и, возможно, другие ... Некоторые из них хороши (требовались жесткие ограничения производительности и памяти), некоторые плохие (мы не обрабатываем исключения должным образом, поэтому лучше не рисковать). Большую часть времени, даже зная, почему они это сделали, на самом деле не ответит на вопрос. Например, если это связано с простотой аудита кода больше, чем на самом деле делает его лучше, это хорошая практика? Вы действительно не можете сказать. Это разные животные, и к ним нужно относиться по-разному.

Все это говорит, это выглядит немного подозрительно для меня, НО :

Решения по безопасности критически важного программного обеспечения и программного обеспечения, вероятно, не должны приниматься незнакомцами при обмене стеками разработки программного обеспечения. Вполне может быть веская причина сделать это, даже если это часть плохой системы. Не читайте слишком много во всем этом, кроме как «пища для размышлений».

drjpizzle
источник
7

Иногда выбрасывать исключение - не лучший метод. Не в последнюю очередь из-за разматывания стека, но иногда из-за проблем с перехватом, особенно в языковых или интерфейсных швах.

Хороший способ справиться с этим - вернуть обогащенный тип данных. У этого типа данных достаточно состояния, чтобы описать все счастливые пути и все несчастные пути. Дело в том, что если вы взаимодействуете с этой функцией (член / глобальный / в противном случае), вы будете вынуждены обрабатывать результат.

Тем не менее, этот обогащенный тип данных не должен заставлять действовать. Представьте себе в своей области пример чего-то вроде var area_calc = new AreaCalculator(); var volume = area_calc.CalculateArea(x, y) * z;. Кажется полезным, volumeдолжен содержать площадь, умноженную на глубину - это может быть куб, цилиндр и т. Д.

Но что, если сервис area_calc был недоступен? Затем area_calc .CalculateArea(x, y)вернул богатый тип данных, содержащий ошибку. Законно ли это умножить на z? Это хороший вопрос. Вы можете заставить пользователей обрабатывать проверку немедленно. Это, однако, нарушает логику обработки ошибок.

var area_calc = new AreaCalculator();
var area_result = area_calc.CalculateArea(x, y);
if (area_result.bad())
{
    //handle unhappy path
}
var volume = area_result.value() * z;

против

var area_calc = new AreaCalculator();
var volume = area_calc.CalculateArea(x, y) * z;
if (volume.bad())
{
    //handle unhappy path
}

По сути, логика разбита на две строки и разделена обработкой ошибок в первом случае, тогда как во втором случае вся соответствующая логика находится в одной строке, за которой следует обработка ошибок.

Во втором случае volumeэто богатый тип данных. Это не просто число. Это увеличивает объем хранилища, и volumeего все равно необходимо исследовать на предмет ошибки. Кроме того, он volumeможет выполнять другие вычисления, прежде чем пользователь решит обработать ошибку, что позволит ей проявиться в нескольких разных местах. Это может быть хорошо или плохо в зависимости от специфики ситуации.

С другой стороны, это volumeможет быть просто простой тип данных - просто число, но что тогда происходит с ошибкой? Возможно, что значение неявно преобразуется, если оно находится в удовлетворительном состоянии. Если он находится в несчастливом состоянии, он может вернуть значение по умолчанию / ошибка (для области 0 или -1 может показаться разумным). С другой стороны, это может вызвать исключение на этой стороне границы интерфейса / языка.

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}
var volume = foo();
if (volume <= 0)
{
    //handle error
}

против

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}

try { var volume = foo(); }
catch(...)
{
    //handle error
}

Выдавая плохое или, возможно, плохое значение, он возлагает большую ответственность на пользователя для проверки данных. Это источник ошибок, потому что для компилятора возвращаемое значение является допустимым целым числом. Если что-то не было проверено, вы обнаружите это, когда что-то пойдет не так. Второй случай сочетает в себе лучшее из обоих миров, позволяя исключениям обрабатывать несчастные пути, в то время как счастливые пути следуют за обычной обработкой. К сожалению, это заставляет пользователя обрабатывать исключения мудро, что сложно.

Просто чтобы прояснить, что Несчастный путь - это случай, неизвестный бизнес-логике (область исключения), отказ от проверки - это счастливый путь, потому что вы знаете, как обрабатывать это с помощью бизнес-правил (область правил).

Конечное решение будет таким, которое допускает все сценарии (в пределах разумного).

  • Пользователь должен иметь возможность запрашивать плохое состояние и обрабатывать его немедленно
  • Пользователь должен иметь возможность работать с обогащенным типом, как если бы был пройден счастливый путь, и распространять подробности ошибки.
  • Пользователь должен иметь возможность извлечь значение «счастливого пути» посредством приведения (неявного / явного в разумных пределах), создавая исключение для несчастных путей.
  • Пользователь должен иметь возможность извлечь значение «счастливого пути» или использовать значение по умолчанию (предоставляется или нет)

Что-то вроде:

Rich::value_type value_or_default(Rich&, Rich::value_type default_value = ...);
bool bad(Rich&);
...unhappy path report... bad_state(Rich&);
Rich& assert_not_bad(Rich&);
class Rich
{
public:
   typedef ... value_type;

   operator value_type() { assert_not_bad(*this); return ...value...; }
   operator X(...) { if (bad(*this)) return ...propagate badness to new value...; /*operate and generate new value*/; }
}

//check
if (bad(x))
{
    var report = bad_state(x);
    //handle error
}

//rethrow
assert_not_bad(x);
var result = (assert_not_bad(x) + 23) / 45;

//propogate
var y = x * 23;

//implicit throw
Rich::value_type val = x;
var val = ((Rich::value_type)x) + 34;
var val2 = static_cast<Rich::value_type>(x) % 3;

//default value
var defaulted = value_or_default(x);
var defaulted_to = value_or_default(x, 55);
Kain0_0
источник
@ TobySpeight Справедливо, эти вещи чувствительны к контексту и имеют диапазон действия.
Kain0_0
Я думаю, что проблема здесь в блоках 'assert_not_bad'. Я думаю, что все закончится тем же местом, что и исходный код, который пытались решить. При тестировании их необходимо заметить, однако, если они действительно являются утверждениями, их следует снять перед производством на реальном самолете. в противном случае некоторые замечательные моменты.
drjpizzle
@drjpizzle Я бы сказал, что если для тестирования было достаточно добавить защиту, то важно, чтобы она оставалась на месте при работе в производстве. Присутствие самого охранника предполагает сомнение. Если вы сомневаетесь в коде, достаточном для его защиты во время тестирования, вы сомневаетесь в этом по технической причине. то есть условие может / действительно может возникнуть. Выполнение тестов не доказывает, что условие никогда не будет достигнуто в производстве. Это означает, что существует известное условие, которое может возникнуть, и где-то нужно что-то обрабатывать. Я думаю, как это обрабатывается, это проблема.
Kain0_0
3

Я отвечу с точки зрения C ++. Я уверен, что все основные понятия могут быть перенесены в C #.

Похоже, ваш предпочтительный стиль - «всегда выбрасывать исключения»

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

Это может быть проблемой для кода C ++, потому что обработка исключений является тяжелой - она ​​заставляет случай сбоя работать медленно и заставляет случай сбоя выделять память (которая иногда даже недоступна) и, как правило, делает вещи менее предсказуемыми. Тяжелый вес EH - одна из причин, по которой вы слышите, как люди говорят что-то вроде «Не используйте исключения для управления потоком».

Поэтому некоторые библиотеки (например, <filesystem>) используют то, что C ++ называет «двойным API», или то, что C # называет Try-Parseшаблоном (спасибо Петру за подсказку!)

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

bool TryCalculateArea(int x, int y, int& result) {
    if (x < 0 || y < 0) {
        return false;
    }
    result = x * y;
    return true;
}

int a1 = CalculateArea(x, y);
int a2;
if (TryCalculateArea(x, y, a2)) {
    // use a2
}

Вы можете сразу увидеть проблему с «двойными API»: много дублирования кода, нет рекомендаций для пользователей относительно того, какой API «правильный» для использования, и пользователь должен сделать трудный выбор между полезными сообщениями об ошибках ( CalculateArea) и speed ( TryCalculateArea), потому что более быстрая версия берет наше полезное "negative side lengths"исключение и превращает его в бесполезное false- «что-то пошло не так, не спрашивайте меня, что или где». (Некоторые сдвоенные интерфейсы используют более выразительный тип ошибки, например, int errnoили Си ++ std::error_code, но это еще не говорит вам , где произошла ошибка - просто , что это действительно происходит где - то.)

Если вы не можете решить, как должен вести себя ваш код, вы всегда можете довести решение до вызывающей стороны!

template<class F>
int CalculateArea(int x, int y, F errorCallback) {
    if (x < 0 || y < 0) {
        return errorCallback(x, y, "negative side lengths");
    }
    return x * y;
}

int a1 = CalculateArea(x, y, [](auto...) { return 0; });
int a2 = CalculateArea(x, y, [](int, int, auto msg) { throw Exception(msg); });
int a3 = CalculateArea(x, y, [](int, int, auto) { return x * y; });

По сути, это то, что делает ваш коллега; за исключением того, что он выделяет «обработчик ошибок» в глобальную переменную:

std::function<int(const char *)> g_errorCallback;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorCallback("negative side lengths");
    }
    return x * y;
}

g_errorCallback = [](auto) { return 0; };
int a1 = CalculateArea(x, y);
g_errorCallback = [](const char *msg) { throw Exception(msg); };
int a2 = CalculateArea(x, y);

Перемещение важных параметров из явных параметров функции в глобальное состояние - почти всегда плохая идея. Я это не рекомендую. (Тот факт, что это не глобальное состояние в вашем случае, а просто государство - член всего экземпляра, немного смягчает проблему, но не сильно.)

Кроме того, ваш коллега излишне ограничивает количество возможных способов обработки ошибок. Вместо того, чтобы разрешить любую лямбду обработки ошибок, он решил только два:

bool g_errorViaException;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorViaException ? throw Exception("negative side lengths") : 0;
    }
    return x * y;
}

g_errorViaException = false;
int a1 = CalculateArea(x, y);
g_errorViaException = true;
int a2 = CalculateArea(x, y);

Это, вероятно, «кислое пятно» любой из этих возможных стратегий. Вы отняли всю гибкость у конечных пользователей, заставив их использовать один из двух ваших обратных вызовов обработки ошибок; и у вас есть все проблемы общего глобального состояния; и вы все еще платите за эту условную ветку везде.

Наконец, общее решение в C ++ (или любом языке с условной компиляцией) будет заключаться в том, чтобы заставить пользователя принимать решение для всей своей программы, глобально, во время компиляции, чтобы неиспользованный кодовый путь можно было полностью оптимизировать:

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
#ifdef NEXCEPTIONS
        return 0;
#else
        throw Exception("negative side lengths");
#endif
    }
    return x * y;
}

// Now these two function calls *must* have the same behavior,
// which is a nice property for a program to have.
// Improves understandability.
//
int a1 = CalculateArea(x, y);
int a2 = CalculateArea(x, y);

Примером того, что работает таким образом, является assertмакрос в C и C ++, который обуславливает свое поведение в макросе препроцессора NDEBUG.

Quuxplusone
источник
Если вместо этого возвращается значение std::optionalfrom TryCalculateArea(), легко объединить реализацию обеих частей двойного интерфейса в единый шаблон-функцию с флагом времени компиляции.
дедупликатор
@ Дедупликатор: Может быть, с std::expected. Если только std::optionalя неправильно пойму ваше предлагаемое решение, оно все равно будет страдать от того, что я сказал: пользователь должен сделать трудный выбор между полезными сообщениями об ошибках и скоростью, потому что более быстрая версия принимает наше полезное "negative side lengths"исключение и превращает его в бесполезное ... false» что-то пошло не так, не спрашивайте меня, что или где. "
Quuxplusone
Вот почему libc ++ <filesystem> на самом деле делает что-то очень похожее на модель коллеги OP: она std::error_code *ecпроходит по всем уровням API, а затем в нижней части выполняет моральный эквивалент if (ec == nullptr) throw something; else *ec = some error code. (Он абстрагирует фактическое ifв нечто, называемое ErrorHandler, но это та же самая основная идея.)
Quuxplusone
Ну, это было бы вариантом сохранить расширенную информацию об ошибках, не бросая. Может быть целесообразным или не стоит потенциальных дополнительных затрат.
дедупликатор
1
Так много хороших мыслей содержится в этом ответе ... Определенно, нужно больше голосов :-)
cmaster
1

Я чувствую, что следует упомянуть, откуда ваш коллега получил их образец

В настоящее время C # имеет шаблон TryGet public bool TryThing(out result). Это позволяет вам получить свой результат, в то же время сообщая, является ли этот результат даже допустимым значением. (Например, все intзначения являются допустимыми результатами для Math.sum(int, int), но если значение было переполнено, этот конкретный результат мог бы быть мусором). Это относительно новая модель, хотя.

Перед outключевым словом вам нужно было либо сгенерировать исключение (дорого, и вызывающий должен его перехватить, либо уничтожить всю программу), создать специальную структуру (класс перед классом или дженерики были действительно полезны) для каждого результата, чтобы представить значение и возможные ошибки (отнимает много времени на создание и распространение программного обеспечения) или возвращает значение по умолчанию "error" (которое не могло быть ошибкой).

Подход, используемый вашим коллегой, дает им возможность заблаговременно исключать ошибки при тестировании / отладке новых функций, одновременно обеспечивая им безопасность и производительность во время выполнения (производительность была критической проблемой все время ~ 30 лет назад), просто возвращая значение ошибки по умолчанию. Теперь это паттерн, в котором было написано программное обеспечение, и ожидаемый паттерн движется вперед, поэтому естественно продолжать делать это таким образом, хотя сейчас есть и лучшие способы. Скорее всего, этот паттерн был унаследован от возраста программного обеспечения или паттерна, в котором ваши колледжи просто не выросли (старые привычки трудно сломать).

Другие ответы уже охватывают, почему это считается плохой практикой, поэтому я просто закончу рекомендовать вам прочитать шаблон TryGet (возможно, также инкапсуляцию того, что объект должен дать своему вызывающему объекту).

Tezra
источник
Перед outключевым словом вы должны написать boolфункцию, которая получает указатель на результат, то есть refпараметр. Вы могли сделать это в VB6 в 1998 году. outКлючевое слово просто покупает вам уверенность во время компиляции, что параметр назначается, когда функция возвращается, и это все, что нужно сделать. Это является хорошим и полезным шаблон , хотя.
Матье Гиндон
@MathieuGuindon Да, но GetTry еще не был хорошо известным / установленным шаблоном, и даже если бы он был, я не совсем уверен, что он был бы использован. В конце концов, отрыв до 2000 года заключался в том, что хранение чего-либо большего, чем 0-99, недопустимо.
Тезра
0

Бывают моменты, когда вы хотите заняться его подходом, но я бы не стал считать их «нормальной» ситуацией. Ключ к определению того, в каком случае вы находитесь:

Его логика / рассуждение заключается в том, что наша программа должна сделать 1 вещь, показать данные пользователю. Любое другое исключение, которое не мешает нам делать это, должно игнорироваться.

Проверьте требования. Если ваши требования фактически говорят о том, что у вас есть одна работа - показывать данные пользователю, то он прав. Однако, по моему опыту, в большинстве случаев пользователю также важно, какие данные отображаются. Они хотят правильные данные. Некоторые системы просто хотят тихо выйти из строя и позволяют пользователю понять, что что-то пошло не так, но я бы посчитал их исключением из правила.

Ключевой вопрос, который я хотел бы задать после сбоя: «Находится ли система в состоянии, когда ожидания пользователя и инварианты программного обеспечения действительны?» Если так, то непременно вернитесь и продолжайте. Практически говоря, это не то, что происходит в большинстве программ.

Что касается самого флага, флаг исключений обычно считается запахом кода, потому что пользователь должен каким-то образом знать, в каком режиме находится модуль, чтобы понять, как работает функция. Если он находится в !shouldThrowExceptionsрежиме, пользователь должен знать, что он отвечает за обнаружение ошибок и поддержание ожиданий и инвариантов при их возникновении. Они также несут ответственность прямо там и тогда, на линии, где вызывается функция. Такой флаг, как правило, сильно сбивает с толку.

Однако это случается. Учтите, что многие процессоры позволяют изменять поведение с плавающей запятой внутри программы. Программа, которая хочет иметь более мягкие стандарты, может сделать это, просто изменив регистр (который фактически является флагом). Хитрость в том, что вы должны быть очень осторожны, чтобы случайно не наступить на чужие пальцы. Код часто проверяет текущий флаг, устанавливает его на желаемую настройку, выполняет операции, а затем устанавливает его обратно. Таким образом, никто не удивится переменам.

Корт Аммон
источник
0

Этот конкретный пример имеет интересную особенность, которая может повлиять на правила ...

CalculateArea(int x, int y)
{
    if(x < 0 || y < 0)
    {
        if(shouldThrowExceptions) 
            throwException;
        else
            return 0;
    }
}

То, что я вижу здесь, это проверка предварительных условий . Сбой проверки предварительных условий подразумевает ошибку выше в стеке вызовов. Таким образом, возникает вопрос: отвечает ли этот код за сообщения об ошибках, находящихся в других местах?

Некоторая напряженность здесь объясняется тем, что этот интерфейс демонстрирует примитивную одержимость - xи yпредположительно должен представлять реальные измерения длины. В контексте программирования, где конкретные типы доменов являются разумным выбором, мы фактически сместили бы проверку предусловий ближе к источнику данных - иными словами, взвалили бы ответственность за целостность данных на стек вызовов, где мы имеем лучший смысл для контекста.

Тем не менее, я не вижу ничего принципиально неправильного в наличии двух разных стратегий управления неудачной проверкой. Я бы предпочел использовать композицию, чтобы определить, какая стратегия используется; флаг функции будет использоваться в корне композиции, а не в реализации метода библиотеки.

// Configurable dependencies
AreaCalculator(PreconditionFailureStrategy strategy)

CalculateArea(int x, int y)
{
    if (x < 0 || y < 0) {
        return this.strategy.fail(0);
    }
    // ...
}

Они работали над критическими приложениями, касающимися авиации, где система не могла выйти из строя.

Национальный совет по дорожному движению и безопасности действительно хорош; Я мог бы предложить альтернативные методы реализации для серых бород, но я не склонен спорить с ними о проектировании переборок в подсистеме отчетов об ошибках.

В более широком смысле: сколько стоит бизнес? Сбой веб-сайта намного дешевле, чем жизненно важной системы.

VoiceOfUnreason
источник
Мне нравится предложение альтернативного способа сохранить желаемую гибкость при модернизации.
drjpizzle
-1

Методы либо обрабатывают исключения, либо нет, в таких языках, как C #, флаги не нужны.

public int Method1()
{
  ...code

 return 0;
}

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

public int Method1()
{
try {  
...code
}
catch {}
 ...Handle error 
}
return 0;
}

В этом случае, если что-то плохое происходит в ... коде, Method1 обрабатывает проблему, и программа должна продолжаться.

Где вы обрабатываете исключения, зависит от вас. Конечно, вы можете игнорировать их, ловя и ничего не делая. Но я хотел бы убедиться, что вы игнорируете только определенные типы исключений, которые вы можете ожидать. Игнорирование ( exception ex) опасно, потому что некоторые исключения, которые вы не хотите игнорировать, как системные исключения, связанные с нехваткой памяти и тому подобное.

Джон Рейнор
источник
3
Текущая настройка, которую опубликовал OP, относится к решению о том, следует ли добровольно генерировать исключение. Код OP не приводит к нежелательному проглатыванию таких вещей, как исключения из памяти. Во всяком случае, утверждение, что исключения приводят к краху системы, подразумевает, что база кода не перехватывает исключения и, следовательно, не будет поглощать любые исключения; и те, которые были и не были преднамеренно выброшены бизнес-логикой ОП.
Флейтер
-1

Такой подход нарушает философию «быстро проваливайся, терпишь неудачу».

Почему вы хотите быстро потерпеть неудачу:

  • Чем быстрее вы терпите неудачу, тем ближе видимый признак сбоя к действительной причине сбоя. Это значительно упрощает отладку - в лучшем случае строка ошибок находится прямо в первой строке трассировки стека.
  • Чем быстрее вы потерпите неудачу (и уловите ошибку соответствующим образом), тем менее вероятно, что вы запутаете остальную часть вашей программы.
  • Чем сложнее вы терпите неудачу (то есть, генерируете исключение вместо того, чтобы просто возвращать код «-1» или что-то в этом роде), тем более вероятно, что вызывающая сторона на самом деле заботится об ошибке, а не просто продолжает работать с неправильными значениями.

Недостатки не неспособность быстро и жестко:

  • Если вы избегаете видимых сбоев, то есть делаете вид, что все в порядке, вы, как правило, усложняете поиск фактической ошибки. Представьте, что возвращаемое значение вашего примера является частью некоторой процедуры, которая вычисляет сумму 100 областей; т.е. вызов этой функции 100 раз и суммирование возвращаемых значений. Если вы молча подавляете ошибку, нет никакого способа найти место, где происходит настоящая ошибка; и все последующие расчеты будут молча неверны.
  • Если вы откладываете ошибку (возвращая невозможное возвращаемое значение, такое как «-1» для области), вы увеличиваете вероятность того, что вызывающая сторона вашей функции просто не будет беспокоиться об этом и забудет обработать ошибку; хотя у них есть информация о сбое под рукой.

Наконец, фактическая обработка ошибок на основе исключений имеет то преимущество, что вы можете выдавать «внешнее» сообщение об ошибке или объект, вы можете легко подключить регистрацию ошибок, предупреждений и т. Д., Не записывая ни одной дополнительной строки в код вашего домена. ,

Таким образом, существуют не только простые технические причины, но также и «системные» причины, которые делают его очень полезным для быстрого отказа.

В конце концов, в наши нынешние времена мы не будем терпеть неудачи, где обработка исключений легка и очень стабильна, а на полпути преступна. Я прекрасно понимаю, откуда приходит мысль, что хорошо подавлять исключения, но это больше не применимо.

Особенно в вашем конкретном случае, когда вы даже можете указать, следует ли создавать исключения или нет: это означает, что вызывающий должен принять решение в любом случае . Таким образом, нет никакого недостатка, чтобы вызывающая сторона перехватила исключение и обработала его соответствующим образом.

Одно замечание в комментарии:

В других ответах указывалось, что быстрый и жесткий сбой нежелателен в критическом приложении, когда аппаратное обеспечение, на котором вы работаете, - это самолет.

Быстрый и жесткий сбой не означает, что все ваше приложение падает. Это означает, что в точке, где происходит ошибка, она локально дает сбой. В примере OP метод низкого уровня, вычисляющий некоторую область, не должен молча заменять ошибку неправильным значением. Это должно потерпеть неудачу ясно.

Некоторый вызывающий по цепочке, очевидно, должен поймать эту ошибку / исключение и обработать ее соответствующим образом. Если этот метод использовался в самолете, это, вероятно, должно привести к тому, что светодиод ошибки погаснет или, по крайней мере, отобразит «область вычисления ошибок» вместо неправильной области.

Anoe
источник
3
В других ответах указывалось, что быстрый и жесткий сбой нежелателен в критическом приложении, когда аппаратное обеспечение, на котором вы работаете, - это самолет.
HAEM
1
@HAEM, то это непонимание того, что неспособность быстро и жесткие средства. Я добавил параграф об этом в ответ.
AnoE
Даже если намерение не состоит в том, чтобы потерпеть неудачу «так сложно», все равно считается рискованным играть с таким огнем.
drjpizzle
Это своего рода точка, @drjpizzle. Если вы привыкли терпеть неудачу быстро, то, по моему опыту, это не «рискованно» или «играть с таким огнем». Au противоречит. Это означает, что вы привыкли думать о «что произойдет, если я получу исключение здесь», где «здесь» означает везде , и вы, как правило, понимаете, будет ли место, в котором вы в данный момент программируете, иметь серьезную проблему ( авиакатастрофа, что бы в этом случае). Играя с огнем, можно ожидать, что все будет в основном нормально, и каждый компонент, сомневаясь, притворяется, что все хорошо ...
AnoE