Перевірено проти "Не перевірено" проти "Винятку" ... Найкраща практика протилежних переконань


10

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

Вимоги до винятків (не в конкретному порядку):

  1. Документація . Мова повинна містити значення для документування винятків, які API може викидати. В ідеалі цей носій документації повинен бути машинопридатним, щоб дозволити компіляторам та IDE надавати підтримку програмісту.

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

    2.1 Помилки в коді, які спричиняють недійсність деяких даних.

    2.2 Проблеми з конфігурацією або іншими зовнішніми ресурсами.

    2.3 Ресурси, які по суті є ненадійними (мережа, файлові системи, бази даних, кінцеві користувачі тощо). Це трохи важливий випадок, оскільки їх ненадійний характер повинен сподіватись на їх спорадичні невдачі. Чи слід вважати ці ситуації винятковими?

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

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

Для висвітлення цих методів на різних мовах були реалізовані наступні методи:

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

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

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

Отже, питання:

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


EDIT: Через кілька хвилин після написання цього питання я наткнувся на це повідомлення , моторошний!


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

"Мова повинна мати значення для документування винятків, які API може викинути." - weeeel. В C ++ "ми" дізналися, що це насправді не працює. Все, що ви можете дуже корисно зробити, - це констатувати, чи може API викинути якесь виняток. (Це дійсно скорочення довгого оповідання, але я думаю, що перегляд noexceptісторії на C ++ може дати дуже хороші уявлення про EH в C # та Java.)
Martin Ba

Відповіді:


10

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

Типи повернення мультисетів чудові, але виняток не замінює. Без винятку код повний шуму, що перевіряє помилки.

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


2
Генеріки допомагають вирішити цілий клас помилок, які здебільшого зумовлені обмеженням підтримки мови парадигмою ОО. все ж, схоже, альтернативи мають або код, який здебільшого робить перевірку помилок, або працює, сподіваючись, що коли-небудь піде не так. Або ви маєте виняткові ситуації, які постійно стоять перед вами, або ви живете в країні мрій пухнастих білих зайчиків, які стають справжніми потворними, коли ви скидаєте великого поганого вовка в середину!
Ньютопський

3
+1 для каскадного випуску. Будь-яка система / архітектура, яка ускладнює зміни, призводить лише до мавпоподібних та брудних систем, незалежно від того, наскільки добре продуманими вони вважали їх.
Матьє М.

2
@Newtopian: Шаблони виконують речі, які неможливо виконати в суворій орієнтації на об'єкти, наприклад, забезпечують безпеку статичного типу для загальних контейнерів.
Девід Торнлі

2
Я хотів би бачити систему виключень із поняттям "перевірені винятки", але одну, що дуже відрізняється від Java. Перевірено-Несс не повинна бути атрибутом виключення типу , а кинути сайти, зловити сайти, а також випадки виключення; якщо метод рекламується як викид перевіреного винятку, він повинен мати два ефекти: (1) функція повинна керувати "кидком" перевіреного винятку, роблячи щось особливе при поверненні (наприклад, встановлюючи прапор перенесення тощо) залежно від точна платформа), до якого потрібно буде підготуватися код виклику.
supercat

7
"Без винятків код повний шуму, що перевіряє помилки.": Я не впевнений у цьому: у Haskell ви можете використовувати монади для цього, і весь шум перевірки помилок зник. Шум, який вводять "багатоступеневі типи повернення", є скоріше обмеженням мови програмування, ніж самим рішенням.
Джорджо

9

Протягом тривалого часу мови OO використання винятків були фактичним стандартом для повідомлення про помилки. Але функціональні мови програмування надають можливість іншого підходу, наприклад, використання монад (яких я не використовував) або більш легкого «програмування, орієнтованого на залізницю», як описав Скотт Влащін.

Це дійсно варіант багатостадійного типу результатів.

  • Функція повертає або успіх, або помилку. Він не може повернути обидва (як у випадку з кортежем).
  • Всі можливі помилки були коротко задокументовані (принаймні, у F # з типами результатів як дискриміновані об'єднання).
  • Абонент не може використовувати результат, не враховуючи, чи був результат успішним чи невдалим.

Тип результату можна оголосити так

type Result<'TSuccess,'TFail> =
| Success of 'TSuccess
| Fail of 'TFail

Отже, результат функції, яка повертає цей тип, буде або a, Successабо Failтип. Це не може бути обом.

У більш імперативно орієнтованих мовах програмування цей тип стилю може вимагати великої кількості коду на сайті виклику. Але функціональне програмування дозволяє побудувати функції зв’язування або операторів, щоб зв'язати між собою кілька функцій, щоб перевірка помилок не займала половини коду. Як приклад:

// Create an updateUser function that takes an id, and new state
// as input, and updates an existing user.
let updateUser id input =
    validateInput input
    >>= loadUser id
    >>= updateUser input
    >>= saveUser id
    >>= notifyAboutUserUpdated

updateUserВиклики функцій кожної з цих функцій послідовно, і кожен з них може не спрацювати. Якщо їм все вдасться, результат останньої викликаної функції повертається. Якщо одна з функцій відмовиться, результат цієї функції буде результатом загальної updateUserфункції. Це все обробляє користувальницький оператор >> =.

У наведеному вище прикладі можуть бути типи помилок

type UserValidationErrorType =
| InvalidEmail of string
| MissingFirstName of string
... etc

type DbErrorType =
| RecordNotFound of int
| ConcurrencyError of int

type UpdateUserErrorType =
| InvalidInput of UserValidationErrorType
| DbError of DbErrorType

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

У Haskell є doпозначення, які можуть зробити код ще чистішим.


2
Дуже хороша відповідь та посилання (залізнично-орієнтоване програмування), +1. Ви можете згадати doпозначення Haskell , що робить отриманий код ще більш чистим.
Джорджіо

1
@Giorgio - Я це зробив зараз, але я не працював з Haskell, лише F #, тому я не міг багато писати про це. Але ви можете додати відповідь, якщо хочете.
Піт

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

2
Це Railway Oriented Programmingсаме монадійна поведінка.
Daenyth

5

Я вважаю , що відповідь Піта дуже хороший, і я хотів би додати трохи уваги та один приклад. Дуже цікаву дискусію щодо використання винятків проти повернення спеціальних значень помилок можна знайти у програмуванні в стандартному ML Роберта Харпера в кінці розділу 29.3, стор. 243, 244.

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

f : ... -> t

і кинути виняток, коли немає можливого результату. Друге рішення - реалізувати функцію з типом

f : ... -> t option

і повернути SOME vуспіх, і NONEневдачу.

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

Які компроміси між двома рішеннями?

  1. Рішення, що базується на типах опцій, явно визначає у типі функції fможливість відмови. Це змушує програміста явно перевірити на відмову, використовуючи аналіз випадку за результатом виклику. Перевірка типу забезпечить, що не можна використовувати t optionтам, деt очікується. Рішення, засноване на винятках, прямо не вказує на збій у своєму типі. Однак програміст все-таки змушений впоратися з відмовою, бо в іншому випадку помилка винятку виключення буде викликана під час виконання, а не під час компіляції.
  2. Рішення, засноване на типах варіантів, вимагає явного аналізу випадку за результатами кожного виклику. Якщо результати "більшості" є успішними, перевірка є зайвою, а отже, надмірно дорогою. Рішення, що базується на винятках, не потребує цих витрат: воно упереджене до «нормального» випадку повернення a t, а не до випадку «невдачі» взагалі не повернення результату . Реалізація винятків гарантує, що використання обробника є більш ефективним, ніж явний аналіз випадку у випадку, коли збій є рідкісним порівняно з успіхом.

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

Це стосується вибору між винятками та типом повернення опціонів.

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

Припустимо, ми хочемо розібрати два числа, а потім ділити перше на друге. Отже, може бути помилка під час розбору кожного числа або при діленні (ділення на нуль). Тому ми маємо перевіряти наявність помилок після кожного кроку.

import Text.Read

parseInt :: String -> Maybe Int
parseInt s = readMaybe s :: Maybe Int

safeDiv :: Int -> Int -> Maybe Int
safeDiv n d = if d /= 0 then Just (n `div` d) else Nothing

toString :: Maybe Int -> String
toString (Just i) = show i
toString Nothing  = "error"

main = do
         -- Get two lines from the terminal.
         nStr <- getLine
         dStr <- getLine

         -- Parse each string and divide.
         let r = do n <- parseInt nStr
                    d <- parseInt dStr
                    safeDiv n d

         -- Print the result.
         putStrLn $ toString r

Розбір і поділ виконуються в let ...блоці. Зауважте, що за допомогою Maybeмонади та doпозначень задається лише шлях до успіху : семантика Maybeмонади неявно поширює значення помилки ( Nothing). Жодних накладних витрат на програміста.


2
Я думаю, що у таких випадках, коли ви хочете надрукувати якесь корисне повідомлення про помилку, Eitherтип буде більш підходящим. Що ви робите, якщо потрапите Nothingсюди? Ви просто отримуєте повідомлення "помилка". Не дуже корисно для налагодження.
сара

1

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

Я прийшов до висновку, що в основному є два типи помилок, з якими має вирішуватися мій код. Є помилки, які перевіряються перед виконанням коду, і є помилки, які не перевіряються перед виконанням коду. Простий приклад помилки, яку можна перевірити перед виконанням коду в NullPointerException.

//... bad code below.  the runnable variable
// tries to call the run() method before the variable
// is instantiated.  Running the code below will cause
// a NullPointerException.
Runnable runnable = null;
runnable.run();

Простий тест міг уникнути помилок, таких як ...

Runnable runnable = null;
...
if (runnable != null)
{   runnable.run(); }

Існують випадки, коли ви можете виконати 1 або більше тестів перед виконанням коду, щоб переконатися, що ви в безпеці, І ВИ ВИ ВИНАГАЄТЕ ВИКОНАННЯ. Наприклад, ви можете протестувати файлову систему, щоб переконатися, що на жорсткому диску є достатньо місця на диску, перш ніж записувати свої дані на диск. У багатопроцесорній операційній системі, як-от ті, що використовуються сьогодні, ваш процес може перевірити наявність дискового простору, і файлова система поверне значення, сказавши, що є достатньо місця, тоді контекстний перемикач на інший процес може записати решта байтів, доступних для операційної система. Коли контекст Операційної системи повернеться до запущеного процесу, де ви записуєте свій вміст на диск, виняток відбудеться просто тому, що у файловій системі недостатньо місця на диску.

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

Бувають випадки, коли стає важко вирішити, перевіряється код чи ні. Наприклад, якщо ви пишете інтерпретатора, і SyntaxException кидається, коли код не виконується з якоїсь синтаксичної причини, чи повинен SyntaxException бути перевіреним винятком або (у Java) RuntimeException? Я відповів би, якщо інтерпретатор перевіряє синтаксис коду перед виконанням коду, тоді Виняток повинен бути RuntimeException. Якщо інтерпретатор просто запускає код "гарячий" і просто потрапляє на синтаксичну помилку, я б сказав, що виняток повинен бути перевіреним винятком.

Я визнаю, що мені не завжди раді ловити або кидати перевірене виняток, бо є час, коли я не впевнений, що робити. Перевірені винятки - це спосіб змусити програміста пам’ятати про можливу проблему, яка може виникнути. Однією з причин, чому я програмую на Java, є те, що вона має перевірені винятки.


1
Я вважаю, що серцевий кардіостимулятор був написаний мовою, яка взагалі не мала винятків, і всі рядки коду обробляли помилки через зворотні коди. Коли ви кидаєте виняток, ви говорите "все пішло не так", і єдиний безпечний спосіб продовжити обробку - це зупинка та перезапуск. Програма, яка так легко потрапляє в недійсний стан, - це не те, що потрібно для критичного програмного забезпечення (а Java явно забороняє його використання для критичного програмного забезпечення в EULA)
gbjbaanb

Використання винятку та не перевірка їх проти використання повернення коду та не перевірка їх врешті-решт, все це призводить до однакової зупинки серця.
Ньютопіан

-1

На даний момент я перебуваю в середині досить великого проекту / API на базі ООП, і я використав цей макет винятків. Але все насправді залежить від того, наскільки глибоко ви хочете пройти, за винятком обробки та подібного.

ОчікуванийЕксцепція
- АвторизованийЕксцепція
- EmptySetException
- NoRemainingException
- NoRowsException
- NotFoundException
- ValidationException

UnexpectedException
- ConnectivityException
- EnvironmentException
- ProgrammerException
- SQLException

ПРИКЛАД

   $valid_types = array('mysql', 'oracle', 'sqlite');
       if (!in_array($type, $valid_types)) {
           throw new ecProgrammerException(
        'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
    $type,
    join(', ', $valid_types)
    );
}

11
Якщо виняток очікується, це насправді не виняток. "NoRowsException"? Звучить, як контрольний потік до мене, і тому погане використання виключення.
квентин-зірин

1
@qes: Є сенс створювати виняток, коли функція не в змозі обчислити значення, наприклад, подвійний Math.sqrt (double v) або User findUser (довгий ідентифікатор). Це дає свободі абоненту можливість ловити та обробляти помилки там, де це зручно, замість перевірки після кожного дзвінка.
Кевін Клайн

1
Очікуваний = контрольний потік = анти-шаблон винятку. Виняток не слід використовувати для контрольного потоку. Якщо очікується помилка для конкретного введення, то вона просто передається як частина повернутого значення. Так ми маємо NANабо NULL.
Еоніл

1
@Eonil ... або Варіант <T>
Maarten Bodewes
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.