Чи добре використовувати винятки як інструменти для раннього «ловлення» помилок?


29

Я використовую винятки, щоб рано наздогнати проблеми. Наприклад:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

Моя програма ніколи не повинна переходити nullна цю функцію. Я ніколи цього не маю намір. Однак, як ми всі знаємо, в програмуванні трапляються випадкові речі.

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

Тепер ви маєте рацію, у цьому випадку це nullпросто спричинить NullPointerExceptionвідразу, тому це може бути не найкращим прикладом.

Але розглянемо, наприклад, такий метод, як цей:

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

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

Зміна такої функції:

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

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

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


Тож моє запитання просто: це хороша практика? Чи добре використовувати виключення як інструменти для запобігання проблемам? Це законне застосування винятків чи це проблематично?


2
Чи може пояснювач пояснити, що з цим питанням не так?
Джорджіо

2
"крах рано, крах часто"
Брайан Чен

16
Саме для цього є винятки.
raptortech97

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

3
Оскільки ви кидаєте IllegalArgumentException, зайве сказати " Вхідна особа".
кевін клайн

Відповіді:


29

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


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

Він також відомий як "
невдалий

8

Так, кидати винятки - це гарна ідея. Киньте їх рано, кидайте їх часто, кидайте їх жадібно.

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

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

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

Я знайшов одне вдосконалення - не кидати лише винятки базового рівня. IllegalArgumentExceptionце добре, але по суті може походити з будь-якого місця. Для додавання спеціальних винятків на більшості мов не потрібно багато. Про свій модуль по роботі з персоналом скажіть:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

Потім, коли хтось бачить PersonArgumentException, зрозуміло, звідки вона береться. Існує акт збалансування того, скільки спеціальних винятків ви хочете додати, тому що ви не хочете без потреби множувати сутності (Оккамова бритва). Часто достатньо лише декількох спеціальних винятків для сигналізації "цей модуль не отримує потрібних даних!" або "цей модуль не може робити те, що повинен робити!" таким чином, що є специфічним та адаптованим, але не настільки точним, що вам доведеться повторно реалізувати всю ієрархію винятків. Я часто стикаюся з тим невеликим набором спеціальних винятків, починаючи з винятків акцій, потім сканую код і розумію, що "ці N місця збільшують винятки з акцій, але вони зводяться до ідеї вищого рівня, що вони не отримують дані їм потрібно; хай '


I've never actually met an application codebase for which I'd want (most) checks removed at runtime. Тоді ви ще не зробили коду, який є критичним для продуктивності. Я зараз працюю над чимось, що робить 37 Мс в секунду з твердженнями, зібраними в і 42 М без них. Твердження не підтверджені зовнішнім входом, вони там, щоб переконатися в правильності коду. Мої клієнти із задоволенням приймають на 13% більше, коли я впевнений, що мої речі не зламані.
Blrfl

Слід уникати перекидання кодової бази за допомогою спеціальних винятків, оскільки, хоча це може допомогти вам, воно не допоможе іншим, хто має з ними ознайомитися. Зазвичай, якщо ви розширюєте поширений виняток і не додаєте жодних членів чи функціональних можливостей, вам насправді це не потрібно. Навіть у вашому прикладі PersonArgumentExceptionне так зрозуміло, як IllegalArgumentException. Останнє загальновідомо, що його кидають при передачі незаконного аргументу. Я насправді очікував, що колишнє буде кинуто, якби Personвиклик був у недійсному стані для дзвінка (подібно до InvalidOperationExceptionC #).
Selali Adobor

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

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

Я провів залежність від продуктивності в числовій / HPC та ОС / середній програмі. Підвищення продуктивності на 13% - це не мала річ. Я можу відключити деякі чеки, щоб отримати його, так само як військовий командир може попросити 105% номінальної продукції реактора. Але я бачив, що "швидше буде працювати так", що часто є причиною відключення чеків, резервного копіювання та інших захистів. Це в основному торгує стійкістю та безпекою (у багатьох випадках, включаючи цілісність даних) для отримання додаткової продуктивності. Чи варто це - це виклик рішення.
Джонатан Юніс

3

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

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

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

Для більш реалістичного прикладу ви можете використовувати assertзаяви, які:

  1. Коротше писати та читати:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. Розроблені спеціально для вашого підходу. У світі, де у вас є і твердження, і винятки, ви можете їх розрізнити так:

    • Ствердження стосуються помилок програмування ("/ * цього ніколи не повинно відбуватися * /"),
    • Винятки становлять випадкові випадки (виняткові, але ймовірні ситуації)

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

Статичний аналізатор (наприклад, компілятор) також може бути щасливішим.

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


1

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

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

Я знаходжу друге рішення, наприклад

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

більш солідний, бо

  1. Він виявляє помилку якнайшвидше, тобто коли registerPerson()викликається, а не коли нульовий виняток вказівника кидається десь у стек виклику. Налагодження стає набагато простіше: всі ми знаємо, наскільки недійсне значення може пройти через код, перш ніж воно проявиться як помилка.
  2. Це зменшує зв'язок між функціями: registerPerson()не робить жодних припущень щодо того, які інші функції в кінцевому підсумку будуть використовувати personаргумент та як вони будуть його використовувати: рішення, яке nullє помилкою, приймається та реалізується локально.

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


1
Надання причини ("Особа, яка вводить, є нульовою") також допомагає зрозуміти проблему, на відміну від того, коли якийсь незрозумілий метод у бібліотеці не вдається.
Флоріан F

1

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

Але давайте розглянемо дещо інший приклад.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

Якщо б не було перевірки аргументів у конструкторі, ви отримали б NullReferenceExceptionпри дзвінку Calculate.

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

Якщо ми видалимо цю явну перевірку аргументів, вам доведеться з’ясувати, чому NullReferenceExceptionсталося, коли Calculateвикликався. І відстеження, чому об’єкт був побудований з nullлюдиною, може бути хитромудрим, особливо якщо код, що конструює калькулятор, не наближений до коду, який насправді викликає Calculateфункцію.


0

Не в наведених прикладах.

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

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

Але загалом слід кидатись рано, як тільки визначиш, що щось пішло не так (при повазі до ДУХОГО). Вони, мабуть, не є великими прикладами цього.


-3

У вашій прикладі функції я вважаю за краще, щоб ви не перевіряли і просто дозволяли це NullReferenceExceptionстатися

Для одного немає сенсу пропускати туди нуль у будь-якому випадку, тож я з’ясую проблему одразу на основі кидання а NullReferenceException.

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

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


Я працюю декілька років на основі коду, який ґрунтується на цьому принципі: покажчики просто де-посилаються, коли це потрібно, і майже ніколи не перевіряються на NULL та обробляються явно. Налагодження цього коду завжди було трудомістким та клопітким, оскільки винятки (або основні демпінги) трапляються набагато пізніше wrt точці, де є логічна помилка. Я витрачав буквально тижні на пошуки певних помилок. З цієї причини я віддаю перевагу стилю "відмовитись якнайшвидше", принаймні для складного коду, де не відразу зрозуміло, звідки походить виняток з нульовим вказівником.
Джорджіо

"Для одного, немає сенсу передавати нуль все одно, тому я негайно з’ясую проблему, виходячи з викидання NullReferenceException.": Не обов'язково, як показує другий приклад Прога.
Джорджіо

"По-друге, якщо кожна функція викидає дещо різні винятки, виходячи з того, які дані очевидно неправильних даних були надані, то досить скоро у вас є функції, які можуть викидати 18 різних типів винятків ...": У цьому випадку ви може використовувати той самий виняток (навіть NullPointerException) у всіх функціях: важливо - кинути рано, а не викидати різний виняток з кожної функції.
Джорджіо

Саме для цього призначені RuntimeExceptions. Будь-яка умова, яка "не повинна відбуватися", але заважає вашому коду працювати, повинна кинути її. Не потрібно оголошувати їх у підписі методу. Але вам потрібно в якийсь момент зловити їх і повідомити, що функція запитуваної дії не вдалася.
Флоріан F

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