Навіщо проектувати сучасну мову без механізму обробки винятків?


47

Багато сучасних мов надають багато функцій обробки винятків , але мова програмування Swift Apple не забезпечує механізм обробки винятків .

Як і я, керуючись винятками, у мене виникають проблеми з обгортанням того, що це означає. Свіфт має твердження і, звичайно, повертає значення; але у мене виникають труднощі уявити, як мій спосіб мислення, заснований на винятках, без винятку переносить світ на світ (і, з цього приводу, чому такий світ бажаний ). Чи є речі, які я не можу зробити такою мовою, як Swift, яку я можу робити за винятками? Чи отримую я щось, втрачаючи винятки?

Як, наприклад, я можу найкраще висловити щось подібне

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

мовою (наприклад, Swift), якій не вистачає обробки винятків?


11
Ви можете додати Перейти до списку , якщо ми просто ігноруємо, panicщо не зовсім те саме. Крім того, що там сказано, виняток - це не набагато більше, ніж складний (але комфортний) спосіб виконання GOTO, хоча з очевидних причин цього ніхто не називає.
JensG

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

1
Я не зовсім слідкую за тобою, @Robert. C ++ вдається підтримувати винятки без вивезення сміття.
Карл Білефельдт

3
@KarlBielefeldt: За великі кошти, з того, що я розумію. Знову ж таки, чи є щось, що робиться в C ++ без великих витрат, принаймні зусиль та необхідних знань домену?
Роберт Харві

2
@RobertHarvey: Добре. Я один з тих людей, хто не думає досить важко про ці речі. Мені пришпили думки, що ARC - це GC, але, звичайно, це не так. Так в основному (якщо я розумію, грубо), винятки були б брудною справою (незважаючи на C ++) у мові, де утилізація об'єктів покладається на контрольний потік?
orome

Відповіді:


33

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

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

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

Я не знайомий зі Свіфт, але мало що я читав про його поводження з помилками говорить про те, що вони призначені для керування помилками, щоб дотримуватися більш функціональних моделей. Я бачив приклади коду з successта failureблоками, які дуже схожі на ф'ючерси.

Ось приклад з використанням Futureвід цього Scala підручника :

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

Ви можете бачити, що вона має приблизно таку ж структуру, як і ваш приклад, використовуючи винятки. futureБлок подібно try. onFailureБлок , як обробник виключень. У Scala, як і в більшості функціональних мов, Futureреалізується повністю з використанням самої мови. Для цього не потрібен спеціальний синтаксис, як це роблять винятки. Це означає, що ви можете визначити власні подібні конструкції. Можливо, додайте timeout, наприклад, блок або функцію реєстрації.

Крім того, ви можете передавати майбутнє навколо, повертати його з функції, зберігати його в структурі даних чи ін. Це першокласне значення. Ви не обмежені як винятки, які потрібно поширювати прямо в стек.

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

Це такі речі, які ви "отримуєте, втрачаючи винятки".


1
Таким чином, Futureце по суті спосіб перевірити значення, повернене з функції, не перестаючи чекати її. Як і Swift, це засноване на поверненні значення, але на відміну від Swift, реакція на повернене значення може виникнути пізніше (трохи схоже на винятки). Правильно?
orome

1
Ви Futureправильно розумієте , але я думаю, що ви можете неправильно характеризувати Свіфта. Дивіться, наприклад, першу частину цієї відповіді на stackoverflow.
Карл Білефельдт

Хм, я новачок у Свіфті, тому мені відповідь трохи важко розібратися. Але якщо я не помиляюся: це, по суті, передає обробник, який можна викликати пізніше; правильно?
orome

Так. Ви в основному створюєте зворотний виклик, коли виникає помилка.
Карл Білефельдт

Eitherбуде кращим прикладом IMHO
Paweł Prażak

19

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

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

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

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

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

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


Чи правильно, що система типу типу Swift, включаючи необов'язкові, - це така "потужна система типів", яка цього досягає?
orome

2
Так, необов'язкові та, загалом, типи сум (їх у Swift / Rust) називають "enum" можуть досягти цього. Однак потрібна додаткова робота, щоб зробити їх приємними у використанні: у Swift це досягається за допомогою додаткового синтаксису ланцюга; в Хаскеллі це досягається монадичною нотацією.
Rufflewind

Чи може "достатньо потужна система типу" дати слід стеку, якщо не зовсім марно
Paweł Prażak

1
+1 для вказівки, що винятки затьмарюють потік контролю. Як допоміжна примітка: Виправдано, чи винятки насправді не є більш злими, ніж goto: gotoОбмеження обмежується однією функцією, що є досить малим діапазоном, якщо функція дійсно невелика, виняток діє скоріше як деякий перехрес gotoі come from(див. Див. en.wikipedia.org/wiki/INTERCAL ;-)). Він може в значній мірі з'єднати будь-які два фрагменти коду, можливо, пропустивши через код деякі треті функції. Єдине, чого він не може зробити, а це gotoможе, - це повернутися назад.
cmaster

2
@ PawełPrażak Під час роботи з великою кількістю функцій вищого порядку сліди стека не є настільки цінними. Чіткі гарантії щодо входів та виходів та уникнення побічних ефектів - це те, що заважає цій непрямій причині плутати помилки.
Джек

11

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

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

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

Тож деякі сучасні мови вирішили, що не варто. Наприклад, іржа має винятки, але їх не можна "спіймати", тому немає коду для взаємодії з об'єктами в недійсному стані.


1
Мета лову - зробити об'єкт послідовним (або припинити щось, якщо це неможливо) після того, як сталася помилка.
JoulinRouge

@JoulinRouge Я знаю. Але деякі мови вирішили не надавати вам такої можливості, а натомість завершити процес. Ці мовні дизайнери знають, який спосіб очищення ви хотіли б зробити, але зробили висновок, що це занадто хитро, щоб вам це зробити, і компроміси, які беруть участь у цьому, не варто. Я усвідомлюю, що ви можете не погодитися з їх вибором ... але цінно знати, що вони зробили вибір свідомо саме з цих причин.
Charlie Flowers

6

Одне, що мене спочатку здивувало мовою Іржі, - це те, що вона не підтримує винятків з лову. Ви можете викидати винятки, але лише час виконання може спіймати їх, коли завдання (продуманий потік, але не завжди окремий потік ОС) гине; якщо ви запускаєте завдання самостійно, то можете запитати, чи нормально воно виходило, чи fail!()редагувало його .

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

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

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}

1
Тож чи справедливо сказати, що це теж (як і підхід Го) схоже на Свіфт, який має assert, але ні catch?
orome

1
У Swift спробуйте! означає: Так, я знаю, що це може вийти з ладу, але я впевнений, що це не вдасться, тому я не обробляю це, і якщо це не вдасться, то мій код невірний, тому, будь ласка, аварійне завершення в такому випадку.
gnasher729

6

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

Винятки - це здебільшого інструмент розробки та спосіб отримати розумні повідомлення про помилки від виробництва у несподіваних випадках.

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

Це власне значення "несподіваного".

Btw., Що очікується, а що ні - це рішення, яке можна прийняти лише на сайті виклику. Ось чому перевірені винятки в Java не вийшли - рішення приймається в момент розробки API, коли зовсім не ясно, що очікується чи несподівано.

Простий приклад: API хеш-карти може мати два способи:

Value get(Key)

і

Option<Value> getOption(key)

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

Кодекс ніколи не повинен намагатися боротися з невдалими базовими припущеннями.

За винятком перевірки їх та викидання добре зрозумілих винятків, звичайно.

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

Лові блоки всюди - дуже погана ідея.

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

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

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

Моделювання їх, виконуючи ВСЕ за допомогою монадичного прив’язки, робить ваш код менш читабельним та зручним для обслуговування, і ви закінчуєтесь без сліду стека, що робить цей підхід НАЙКУЩО гіршим.

Функціональна помилка обробки помилок чудово підходить для очікуваних випадків помилок.

Нехай обробка виключень автоматично береться за всі інші, ось для чого це :)


3

Свіфт тут використовує ті самі принципи, що і «Objective-C», лише більш послідовно. У Objective-C винятки вказують на помилки програмування. Вони не обробляються, за винятком інструментів звітування про аварії. "Обробка винятків" здійснюється шляхом фіксації коду. (Є деякі винятки з ах, наприклад, у міжпроцесорних комунікаціях. Але це досить рідко, і багато людей ніколи не наштовхуються на це. І в Objective-C насправді є спроба / зловити / нарешті / кинути, але ви їх рідко використовуєте). Свіфт просто видалив можливість ловити винятки.

У Swift є функція, яка схожа на обробку винятків, але є лише примусовою обробкою помилок. Історично в Objective-C була досить поширена схема поводження з помилками: метод буде повертати BOOL (ТАК для успіху) або посилання на об'єкт (нуль для відмови, не нуль на успіх), і матиме параметр "вказівник на NSError *" який буде використовуватися для зберігання посилання на NSError. Swift автоматично перетворює виклики на такий метод у щось, схоже на обробку винятків.

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


Для Свіфта це правильна відповідь, ІМО. Swift повинен залишатися сумісним з існуючими системними рамками Objective-C, тому під кришкою у них немає традиційних винятків. Нещодавно я писав повідомлення в блозі про те, як працює ObjC для обробки помилок: orangejuiceliberationfront.com/…
uliwitness

2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

У C ви б закінчилися чимось подібним до вище.

Чи є у Swift речі, які я не можу зробити за винятками?

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


І, також, ті заклики ...throw_ioerror()повертати помилки, а не кидати винятки?
orome

1
@raxacoricofallapatorius Якщо ми стверджуємо, що винятків не існує, я припускаю, що програма слідує звичайній схемі повернення кодів помилок на помилку та 0 на успіх.
stonemetal

1
@stonemetal Деякі мови, як-от Rust та Haskell, використовують типову систему, щоб повернути щось більш значуще, ніж код помилки, без додавання прихованих точок виходу, як це роблять винятки. Наприклад, функція Rust може повернути Result<T, E>перерахунок, який може бути або an Ok<T>, або an Err<E>, якщо Tвін є типом, якого ви хотіли, якщо такий є, і Eбути типом, що представляє помилку. Узгодження шаблону та деякі конкретні методи спрощують вирішення як успіхів, так і невдач. Коротше кажучи, не вважайте, що відсутність виключень автоматично означає коди помилок.
8bittree

1

Окрім відповіді Чарлі:

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

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

Наприклад, коли вам доведеться мати справу з IO, використовуючи деяку криптографію, у вас може бути 20 видів винятків, які можуть бути викинуті з 50 методів. Уявіть кількість потрібного коду для обробки виключень. Обробка винятків займе в кілька разів більше коду, ніж сам код.

Насправді ви знаєте, коли виняток не може з’являтися, і вам просто ніколи не потрібно писати стільки обробку винятків, тому ви просто використовуєте деякі обхідні шляхи, щоб ігнорувати оголошені винятки. У моїй практиці лише близько 5% оголошених винятків потрібно обробляти в коді, щоб мати надійну програму.


Ну, насправді з цими винятками часто можна впоратися лише в одному місці. Наприклад, у функції "оновлення даних для завантаження", якщо SSL виходить з ладу або DNS є нерозв’язним або веб-сервер повертає 404, це не має значення, введіть його вгорі і повідомте про помилку користувачеві.
Zan Lynx
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.