Маючи прапор, щоб вказати, чи варто нам кидати помилки


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/…
bic

1
Окрім коментаря Try-Parse від Пітера, ось приємна стаття про винятки Vexing
Лінаїт

2
"Таким чином, ми не хотіли б викидати виняток (а не впоратися з ним?) І змусити його зняти програму, коли для користувача вона працює добре" - ви знаєте, що ви можете зловити винятки?
користувач253751

1
Я впевнений, що це було висвітлено раніше десь в іншому місці, але, зважаючи на простий приклад області, я б більше схилявся цікавитись, звідки беруться ці негативні цифри і чи зможете ви впоратися з цією умовою помилки десь в іншому місці (наприклад, що б не читав файл, що містить довжину та ширину, наприклад); Однак "ми намагаємось використовувати мережевий пристрій, який не може бути присутнім у той час". Точка могла б заслужити зовсім іншу відповідь, це третій API чи щось галузевий стандарт, такий як TCP / UDP?
jrh

Відповіді:


74

Проблема такого підходу полягає в тому, що хоча винятки ніколи не викидаються (і, таким чином, програма ніколи не виходить з ладу через невиконані винятки), результати, що повертаються, не обов'язково є правильними, і користувач може ніколи не знати, що є проблема з даними (або що це за проблема і як її виправити).

Для того, щоб результати були правильними та значущими, метод виклику повинен перевірити результат на спеціальні числа - тобто конкретні зворотні значення, що використовуються для позначення проблем, що виникли під час виконання методу. Негативні (або нульові) числа, що повертаються для певних додатних величин (наприклад, область), є яскравим прикладом цього у старішому коді. Якщо метод виклику не знає (або забуває!) Перевірити наявність цих спеціальних номерів, однак обробка може продовжуватися, не розуміючи помилки. Потім дані відображаються користувачеві, показуючи площу 0, що користувач знає, що невірно, але у них немає вказівки, що пішло не так, де чи чому. Потім вони задаються питанням, чи помиляється будь-яке з інших значень ...

Якщо виняток буде кинуто, обробка припиниться, помилка (в ідеалі) буде записана в журнал, і користувач може бути повідомлений якимось чином. Потім користувач може виправити все, що не так, і спробувати ще раз. Правильне поводження з винятками (і тестування!) Забезпечить, щоб критичні програми не виходили з ладу або іншим чином не закінчувались недійсним станом.


1
@Quirk Вражаюче, як Чен зумів порушити Принцип єдиної відповідальності лише за 3 чи 4 рядки. У цьому справжня проблема. Крім того, проблема, про яку він говорить (програміст не може думати про наслідки помилок у кожному рядку) - це завжди можливість з неперевіреними винятками і лише іноді можливість із перевіреними винятками. Я думаю, я бачив усі аргументи, коли-небудь зроблені проти перевірених винятків, і жоден з них не є дійсним.
TKK

@TKK особисто є випадки, в які я натрапив на перевірку винятків у .NET. Було б добре, якби були якісь інструменти статичного аналізу високого класу, які могли б переконатися, що документи API, як викинуті винятки, є точними, хоча це, можливо, буде дуже неможливим, особливо при доступі до рідних ресурсів.
jrh

1
@jrh Так, було б непогано, якби щось увімкнуло якусь безпеку винятків у .NET, подібно до того, як TypeScript додає безпеку типу в JS.
TKK

47

Це хороший спосіб поводження з винятками?

Ні, я думаю, що це досить погана практика. Закидання винятку проти повернення значення - це фундаментальна зміна в API, зміна підпису методу та примушення методу вести себе зовсім інакше з точки зору інтерфейсу.

Загалом, коли ми розробляємо класи та їх API, ми повинні це враховувати

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

  2. через введення залежності та будь-яку кількість інших практик програмування один споживач-клієнт може створювати об'єкти та передавати їх іншим іншим користувачам - тому часто ми маємо поділ між творцями об’єктів та користувачами об’єктів.

Поміркуйте тепер, що повинен зробити метод, що викликає абонент, щоб використовувати екземпляр, який йому передали, наприклад, для виклику методу обчислення: абонент повинен перевірити наявність площі нульового рівня, а також винятки вилучення - ouch! Міркування про тестування стосуються не лише самого класу, але й керування помилками абонентів ...

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

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

Використовуючи два різні класи або два різні методи, ви можете використовувати IDEs знайти користувачів методу та функції рефактора тощо. Набагато простіше, оскільки два випадки використання більше не пов'язані. Читання, написання, обслуговування, огляди та тестування коду також простіші.


З іншого боку, я особисто вважаю, що ми не повинні приймати булеві параметри конфігурації, де фактичні абоненти просто передають константу . Така параметризація конфігурації поєднує два окремі випадки використання без жодної реальної користі.

Погляньте на свою кодову базу і перевірте, чи застосовується колишня змінна (або незмінне вираження) для булевого параметра конфігурації в конструкторі! Я сумніваюся в цьому.


Далі слід розглянути питання, чому обчислювальна область може провалюватися. Найкраще може бути кинути конструктор, якщо розрахунок неможливо зробити. Однак якщо ви не знаєте, чи можна зробити обчислення до подальшої ініціалізації об'єкта, можливо, розгляньте можливість використання різних класів для диференціації цих станів (не готова до обчислення області проти готової до обчислення області).

Я читав, що ваша ситуація з відмовою орієнтована на усунення, тому може не застосовуватися; просто їжа для роздумів.


Чи не повинно відповідач, який телефонує, визначати, як продовжувати?

Так, я згоден. Зрозуміло, що абонент вважає передчасним вирішити, що область 0 - це правильна відповідь за умов помилки (тим більше, що 0 - це дійсна область, тому жоден спосіб не визначити різницю між помилкою та фактичною 0, хоча може не стосуватися вашої програми).


Вам не потрібно взагалі перевіряти виняток, оскільки вам потрібно перевірити аргументи перед тим, як викликати метод. Перевірка результату на нуль не відрізняє юридичних аргументів 0, 0 та незаконних негативних. API справді жахливий IMHO.
BlackJack

Додатки MS MS для іонів C99 і C ++ є прикладами API, де гачок або прапор докорінно змінює реакцію на відмову.
Дедуплікатор

37

Вони працювали над критичними програмами, що стосуються авіації, де система не могла знизитися. Як результат ...

Це цікаве вступ, який справляє враження, що мотивація цієї конструкції полягає у тому, щоб уникнути викидів у деяких контекстах, "оскільки система могла б згортати" . Але якщо система "може вийти з ладу через виняток", це явна ознака цього

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

Тож якщо програма, яка використовує функцію " AreaCalculatorбаггі", то ваш колега вважає за краще не мати програму "рано в збої", а повернути якесь неправильне значення (сподіваючись, що ніхто цього не помітить, або сподіваючись, що ніхто не зробить з цим чогось важливого). Це насправді маскує помилку , і на мій досвід, вона рано чи пізно призведе до подальших помилок, для яких стає важко знайти першопричину.

Запис програми IMHO, яка не дає збоїв за жодних обставин, але показує неправильні дані або результати розрахунку, як правило, нічим не краще, ніж дозволити програмі збій. Єдиний правильний підхід - дати можливість абоненту помітити помилку, поправити її, нехай він вирішить, чи потрібно інформувати користувача про неправильну поведінку, і чи безпечно продовжувати будь-яку обробку, чи безпечніше щоб повністю зупинити програму. Таким чином, я рекомендую одне з наступних:

  • зробити це важко не помітити той факт, що функція може кинути виняток. Тут є вашим другом стандарти документації та кодування, і регулярні перегляди коду повинні підтримувати правильне використання компонентів та правильну обробку винятків.

  • навчіть команду очікувати та мати справу з винятками, коли вони використовують компоненти "чорної скриньки", і мати на увазі глобальну поведінку програми.

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

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

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


3
"написання програми, яка не зазнає аварії за жодних обставин, але показує неправильні дані або результати розрахунку, як правило, нічим не краще, ніж дозволити програмі зазнати краху" Я цілком погоджуюся в цілому, хоча я міг би уявити, що в авіації я, мабуть, вважаю за краще мати літак як і раніше з інструментами, що показують неправильні значення, порівняно з тим, як було вимкнено комп'ютер літака. Для всіх менш критичних програм, безумовно, краще не маскувати помилки.
Триларіон

18
@Trilarion: якщо програма для льотного комп’ютера не містить належного керування винятками, "виправлення цього", примушуючи компоненти не кидати винятки, є дуже помилковим підходом. Якщо програма виходить з ладу, повинна бути якась резервна система резервного копіювання, яка може перейняти її. Якщо програма не виходить з ладу і показує неправильну висоту, наприклад, пілоти можуть подумати, що "все добре", поки літак мчить у наступну гору.
Док Браун

7
@Trilarion: якщо льотний комп'ютер показує неправильну висоту, і літак вийде з ладу через це, він вам теж не допоможе (особливо, коли система резервного копіювання є там і не отримує інформацію про це потрібно перейняти). Системи резервного копіювання комп’ютерів літака - це не нова ідея, Google для "резервних систем резервного комп'ютерного комп'ютера", я впевнений, що інженери у всьому світі завжди вбудовували надлишкові системи в будь-яку реальну життєво важливу систему (і якби це не було через те, що не втратили страхування).
Док Браун

4
Це. Якщо ви не можете собі дозволити збій програми, ви також не можете дозволити, щоб вона мовчки давала неправильні відповіді. Правильна відповідь - це відповідне поводження з винятками у всіх випадках. Для веб-сайту це означає глобальний обробник, який перетворює несподівані помилки в 500. Можливо, ви також матимете додаткові обробники для більш конкретних ситуацій, як-от наявність циклу в try/ catchв циклі, якщо вам потрібно продовжити обробку, якщо один елемент не працює.
jpmc26

2
Отримати неправильний результат - це завжди найгірший вид невдачі; це нагадує мені правило про оптимізацію: "виправте це перед тим, як оптимізувати, оскільки швидше отримання неправильної відповіді все одно нікому не вигідно".
Toby Speight

13

Написання тестів на одиниці стає дещо складнішим, оскільки вам доведеться кожен раз перевіряти прапор виключення.

Будь-яку функцію з n параметрами перевірити буде важче, ніж функцію з n-1 параметрами. Розгорніть це на абсурд, і аргумент стає тим, що функції взагалі не повинні мати параметрів, тому що це робить їх найпростішим для тестування.

Хоча це чудова ідея написання коду, яку легко перевірити, це жахлива ідея поставити тестову простоту над кодом написання, корисним для людей, які мають її зателефонувати. Якщо приклад у запитанні має комутатор, який визначає, кидають виняток чи ні, можливо, кількість абонентів, які бажають такої поведінки, заслуговує на додавання його до функції. Де межа між складною і занадто складною брехнею є закликом судження; той, хто намагається сказати вам, є яскрава лінія, яка застосовується у будь-яких ситуаціях, слід дивитися з підозрою.

Крім того, якщо щось піде не так, чи не хотіли б ви знати це відразу?

Це залежить від вашого неправильного визначення. Приклад у запитанні визначає неправильно як "заданий розмір менше нуля і shouldThrowExceptionsє істинним". Надання розмірності, якщо менше нуля, не помиляється, коли shouldThrowExceptionsпомилково, оскільки перемикач викликає іншу поведінку. Тобто, просто, не виняткова ситуація.

Справжня проблема тут полягає в тому, що комутатор був названий погано, оскільки він не описує те, що змушує цю функцію робити. Якби йому було присвоєно кращу назву, як treatInvalidDimensionsAsZeroби ви поставили це питання?

Чи не повинно відповідач, який телефонує, визначати, як продовжувати?

Зухвалий чи визначити , як продовжити. У цьому випадку це робиться достроково, встановивши або очистивши, shouldThrowExceptionsі функція поводиться відповідно до свого стану.

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

Його логіка / міркування полягає в тому, що нашій програмі потрібно зробити 1 річ, показати дані користувачеві. Будь-який інший виняток, який не заважає нам це робити, слід ігнорувати. Я погоджуюсь, що їх не слід ігнорувати, але вони повинні підкидати і поводитися з ними відповідною людиною, а не мати для цього справи з прапорами.

Знову ж таки, не існує рішення одного розміру. У прикладі функція була, імовірно, записана у специфікацію, яка говорить про те, як боротися з негативними розмірами. Останнє, що ви хочете зробити, - це зменшити співвідношення сигнал-шум у ваших журналах, заповнивши їх повідомленнями, в яких написано "нормально, тут буде викинуто виняток, але абонент сказав, що не турбувати".

І як один із тих значно старших програмістів, я просив би вас люб’язно відійти від моєї галявини. ;-)


Я погоджуюся, що іменування та наміри є надзвичайно важливими, і в цьому випадку правильна назва параметра може дійсно перетворити таблиці, тому +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 Я думаю, що припущення полягає в тому, що "показати дані" означає "показати правильні дані". Опитуючий не каже, що готовий продукт не працює, просто може бути написано "неправильно". Мені довелося б побачити деякі важкі дані щодо другого пункту. У моєму теперішньому проекті є кілька функцій, що активно використовуються, які мають перемикач "кидок / не кидок" і не важче їх міркувати, ніж будь-яка інша функція, але це одна точка даних.
Blrfl

Хороша відповідь, я думаю, що ця логіка застосовується до набагато ширшого кола обставин, ніж лише до ОП. До речі, нова версія cpp має саме ці причини. Я знаю, що є невеликі відмінності, але ...
drjpizzle

8

Критичний для безпеки та "нормальний" код може призвести до дуже різних уявлень про те, як виглядає "хороша практика". Існує багато перекриттів - деякі речі є ризиковими і їх слід уникати в обох - але все ж є суттєві відмінності. Якщо ви додасте вимогу гарантовано реагувати, ці відхилення виходять досить істотними.

Вони часто стосуються речей, які ви очікували:

  • Що стосується git, неправильна відповідь може бути дуже поганою щодо: тривалого / перериваючого / вивішування або навіть збоїв (які фактично не стосуються, наприклад, зміни випадкового коду випадково).

    Однак: на приладовій панелі, що має розрахунок сили g, що зупиняється і не дозволяє зробити розрахунок швидкості повітря, може бути неприйнятним.

Деякі з них менш очевидні:

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

  • Бути демонстративно безпечним - відносно важливо. Не багато клієнтів сідатимуть, щоб розглянути питання про те, безпечне чи ні джерело, яке вони купують. Якщо ви знаходитесь на авіаційному ринку з іншого боку ...

Як це стосується вашого прикладу:

Не знаю. Існує цілий ряд продуманих процесів, які можуть спричинити такі правила, як "Ніхто не кидає виробничий код" у критичному коді безпеки, який був би досить нерозумним у більш звичних ситуаціях.

Деякі стосуватимуться вбудованої, деякі безпеки та, можливо, інші ... Деякі хороші (потрібні жорсткі межі роботи / пам'яті), деякі погані (ми не обробляємо винятки належним чином, тому краще не ризикувати). Більшість випадків навіть знаючи, чому вони це зробили, насправді не дасть відповіді на питання. Наприклад, якщо це стосується простоти аудиту коду більше того, що насправді робить його кращим, це хороша практика? Ви справді не можете сказати. Вони різні тварини і потребують по-різному поводження.

Все сказане, мені здається, що мене підозрює, АЛЕ :

Важливі рішення щодо програмного забезпечення та програмного забезпечення для програмного забезпечення, мабуть, не повинні приймати незнайомі люди на стадії програмно-інженерної роботи Це може бути вагомою причиною для цього, навіть якщо це частина поганої системи. Не читайте більше нічого іншого, окрім як «їжі для роздумів».


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
}

vs.

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

@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.


Якщо повернути a std::optionalз, TryCalculateArea()замість цього, просто уніфікувати реалізацію обох частин подвійного інтерфейсу в єдиному шаблоні функції з прапором компіляції-часу.
Дедуплікатор

@Deduplicator: Можливо, із a std::expected. Щойно std::optional, якщо я неправильно зрозумію запропоноване вам рішення, воно все одно постраждає від того, що я сказав: користувач повинен зробити важкий вибір між корисними повідомленнями про помилки та швидкістю, тому що більш швидка версія приймає наше корисне "negative side lengths"виняток і згладжує його в непотрібне false- " щось пішло не так, не питайте мене, що і де ".
Quuxplusone

Ось чому libc ++ <filesystem> насправді робить щось дуже близьке до структури колеги ОП: він передає 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ключовим словом вам або довелося викинути виняток (дорого, і абонент повинен зловити його, або вбити всю програму), створити спеціальну структуру (клас перед класом або генерикою справді був річчю), щоб кожен результат представляв значення і можливі помилки (затрата часу на виготовлення та здуття програмного забезпечення), або повернення за замовчуванням значення "помилки" (можливо, це не було помилкою).

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

Інші відповіді вже висвітлюють, чому це вважається поганою практикою, тому я закінчу лише рекомендувати вам прочитати на шаблоні TryGet (можливо, також інкапсуляція того, що обіцяє об’єкт повинен зробити його абоненту).


Перед outключовим словом слід написати boolфункцію, яка приймає покажчик на результат, тобто refпараметр. Ви можете це зробити в VB6 в 1998 році. outКлючове слово просто купує вам впевненість у складанні часу, що параметр призначається, коли функція повертається, це все, що їй належить. Це є хорошим і корисним шаблон , хоча.
Матьє Гіндон

@MathieuGuindon Yeay, але GetTry ще не був добре відомим / усталеним малюнком, і навіть якби він був, я не зовсім впевнений, що він би використовувався. Зрештою, частина прихильності до Y2K полягала в тому, що зберігання нічого більшого від 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);
    }
    // ...
}

Вони працювали над критичними програмами, що стосуються авіації, де система не могла знизитися.

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

Більш широко: яка вартість бізнесу? Збійти веб-сайт набагато дешевше, ніж це життєво важлива система.


Мені подобається пропозиція альтернативного способу зберегти бажану гнучкість, продовжуючи модернізуватися.
drjpizzle

-1

Методи або обробляють винятки, або їх немає, немає необхідності в прапорах на таких мовах, як C #.

public int Method1()
{
  ...code

 return 0;
}

Якщо в коді щось піде не так, тоді цей виняток повинен обробляти абонент. Якщо помилку ніхто не обробляє, програма припиняється.

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

У такому випадку, якщо в ... коді трапиться щось погане, метод1 обробляє проблему і програма повинна продовжувати роботу.

Де ви обробляєте винятки, вирішувати тільки ви. Звичайно, ви можете їх ігнорувати, ловлячи і нічого не роблячи. Але я би переконався, що ви ігноруєте лише певні конкретні типи винятків, які, на вашу думку, можуть виникнути. Ігнорування ( exception ex) небезпечно тим, що деякі винятки, які ви не хочете ігнорувати, як системні винятки щодо пам'яті тощо.


3
Поточна установка, яку опублікувала ОП, стосується вирішення питання про те, чи потрібно охоче кидати виняток. Код OP не призводить до небажаного ковтання речей, як-от винятків із пам'яті. Якщо що-небудь, твердження про те, що винятки руйнують систему, означає, що база коду не вловлює винятки і, таким чином, не поглинає жодних винятків; як ті, що були, так і не були кинуті діловою логікою ОП навмисно.
Флатер

-1

Такий підхід порушує філософію "не швидко, не вдається".

Чому ви хочете швидко провалитися:

  • Чим швидше ви невдачі, тим ближче видимий симптом невдачі до фактичної причини відмови. Це значно спрощує налагодження - в кращому випадку ви маєте рядок помилки прямо в першому рядку трасування стека.
  • Чим швидше ви провалитесь (і вкажете помилку належним чином), тим менше ймовірність, що ви плутаєте решту програми.
  • Чим важче вам не вдатися (тобто викинути виняток, а не просто повернути код "-1" чи щось подібне), тим більше ймовірність, що абонент насправді піклується про помилку, а не просто продовжує працювати з неправильними значеннями.

Недоліки НЕ нездатність швидко і жорстко:

  • Якщо ви уникаєте видимих ​​збоїв, тобто робите вигляд, що все в порядку, вам, як правило, важко знайти фактичну помилку. Уявіть, що повернене значення вашого прикладу є частиною звичайної програми, яка обчислює суму 100 областей; тобто виклик цієї функції 100 разів і підсумовування повернених значень. Якщо ви мовчки пригнічуєте помилку, взагалі немає способу знайти, де відбувається фактична помилка; і всі наступні розрахунки будуть мовчки помилятися.
  • Якщо ви затримуєте провал (повертаючи неможливе значення повернення на зразок "-1" для району), ви збільшуєте ймовірність того, що абонент вашої функції просто не турбується про це і забуває обробляти помилку; навіть якщо вони мають інформацію про збій під рукою.

Нарешті, фактична обробка помилок на основі винятку має ту перевагу, що ви можете надіслати повідомлення про помилку або об'єкт "поза діапазону", ви можете легко підключити реєстрацію помилок, оповіщення тощо, не записуючи жодного додаткового рядка у вашому доменному коді .

Отже, існують не тільки прості технічні причини, але й "системні" причини, які дуже корисно швидко вийти з ладу.

Врешті-решт, не дуже важко та швидко в наші дні, коли обробка винятків є легкою та дуже стабільною - це лише наполовину злочин. Я прекрасно розумію, звідки походить думка про те, що добре придушувати винятки, але це більше не застосовується.

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

Один момент, що виникає у коментарі:

В інших відповідях було зазначено, що в критичних програмах не дуже бажано швидко і важко, коли обладнання, на якому ви працюєте, - це літак.

Невдача швидко і важко не означає, що вся ваша програма не працює. Це означає, що в місці, де виникає помилка, вона локально виходить з ладу. У прикладі ОП метод низького рівня обчислення деякої площі не повинен мовчки замінювати помилку неправильним значенням. Вона повинна чітко провалюватися.

Деякий, хто викликає ланцюг, очевидно, повинен вловлювати цю помилку / виняток і обробляти її належним чином. Якщо цей метод застосовувався в літаку, це, ймовірно, може призвести до засвічення світлодіодного помилки або, принаймні, до відображення "області обчислення помилок" замість неправильної області.


3
В інших відповідях було зазначено, що в критичних програмах не дуже бажано швидко і важко, коли обладнання, на якому ви працюєте, - це літак.
HAEM

1
@HAEM, то це непорозуміння щодо того, що означає невдача швидких і важких. Я відповів абзацом про це у відповідь.
AnoE

Навіть якщо намір не зазнати "такої важкості", все-таки вважається ризикованим грати з таким вогнем.
drjpizzle

Такий сенс, @drjpizzle. Якщо ви звикли швидко виходити з ладу, це, на мій досвід, не є "ризикованим" або "граючим з таким вогнем". Au contraire. Це означає, що ви звикли думати про те, "що трапиться, якби я потрапив до винятку тут", де "тут" означає скрізь , і ти, як правило, усвідомлюєш, чи буде місце, де ти зараз програмуєш, мати велику проблему ( авіакатастрофа, як би там не було). Граючи з вогнем, можна було б очікувати, що все в основному буде добре, і кожен компонент, сумніваючись, робить вигляд, що все добре ...
AnoE
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.