Винятки, коди помилок та дискриміновані об'єднання


80

Нещодавно я почав роботу з програмування на C #, але в мене є досить багато досвіду в Haskell.

Але я розумію, що C # - це об'єктно-орієнтована мова, я не хочу притискати круглий кілок до квадратного отвору.

Я читав статтю " Викидання викидів від Microsoft", де зазначено:

НЕ повертайте коди помилок.

Але, використовуючи Haskell, я використовував тип даних C # OneOf, повертаючи результат як значення "право" або помилка (найчастіше перерахування) як значення "ліворуч".

Це дуже схоже на конвенцію EitherХаскелла.

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

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

Але це, мабуть, не підходить.

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


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

Зауважте, що я не використовую OneOfтакі речі, як "Out Of Memory". Умови, від яких я не очікую відновитись, все одно будуть кидати винятки. Але я відчуваю цілком обґрунтовані проблеми, такі як введення користувачів, яке не розбирає, по суті є "контрольним потоком" і, ймовірно, не повинно кидати винятків.


Наступні думки:

З цього обговорення я зараз забираю:

  1. Якщо ви очікуєте, що catchбільшість часу негайний абонент почне обробляти виняток і продовжить його роботу, можливо, через інший шлях, він, ймовірно, повинен бути частиною типу повернення. Optionalабо OneOfможе бути тут корисним.
  2. Якщо ви очікуєте, що безпосередній абонент не вловлює виняток більшу частину часу, киньте виняток, щоб зберегти безглуздість вручну передавати його в стек.
  3. Якщо ви не впевнені, що збирається зробити безпосередній абонент, можливо, надайте і те, Parseі інше TryParse.

13
Використовуйте F # Result;-)
Astrinus

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

9
Пов'язана стаття: Ерік Ліпперт про чотири "відра" винятків . Приклад, який він наводить щодо екзогенних сприйнять, показує, що існують сценарії, коли вам просто доведеться мати справу з винятками, тобто FileNotFoundException.
Søren D. Ptæus

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

9
Як же ні з відповідей не прояснює , що Монада не код помилки , і ні ? Вони принципово різні, і, отже, питання, як видається, засноване на непорозумінні. (Хоча в модифікованій формі це все-таки актуальне питання.)EitherOneOf
Конрад Рудольф

Відповіді:


131

але збій програмного забезпечення вашого клієнта все ще не є хорошою справою

Це, звичайно, хороша річ.

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

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

Це абсолютно правда, але це часто неправильно зрозуміло. Коли вони винайшли систему виключень, вони побоювалися, що вони порушують структуроване програмування. Структурне програмування тому у нас є for, while, until, break, і , continueколи все , що потрібно, щоб зробити все , що є goto.

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

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

Принципово винятки стосуються відкидання припущення. Коли ви просите зберегти файл, ви припускаєте, що файл може бути і буде збережений. Виняток, який ви отримуєте, коли це не може бути причиною того, що ім’я є незаконним, HD є повним або через те, що щур прогризав ваш кабель даних. Ви можете обробляти всі ці помилки по-різному, ви можете їх обробляти однаково або ви можете дозволити їм зупинити систему. У вашому коді є щасливий шлях, де ваші припущення мають бути справдими. Так чи інакше винятки знімають вас із того щасливого шляху. Строго кажучи, так, це свого роду "контроль потоку", але це не те, про що вони вас попереджали. Вони говорили про дурниці так :

введіть тут опис зображення

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

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

НЕ повертайте коди помилок.

... є прекрасною порадою, коли ви знаходитесь у кодовій базі, яка зазвичай не використовує коди помилок. Чому? Тому що ніхто не пам’ятатиме, щоб зберегти повернене значення та перевірити ваші коди помилок. Це все ще прекрасна умова, коли ти в С.

OneOf

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

Мені подобається конвенція сама. Одне з найкращих пояснень цього я знайшов тут * :

введіть тут опис зображення

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

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


Наступні думки:

З цього обговорення я зараз забираю:

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

Це насправді не так просто. Одне з фундаментальних речей, яке вам потрібно зрозуміти - це нуль.

Скільки днів залишилось у травні? 0 (бо це не травень. Це вже червень).

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

Наприклад 2 :

IList<int> ParseAllTheInts(String s) { ... }

Чи можете ви придумати якусь вагому причину, щоб це було розроблено так, щоб воно навмисно кидало що-небудь коли-небудь? Здогадайтесь, що ви отримуєте, коли жоден інт не може бути розбір? Мені навіть не потрібно говорити тобі.

Це знак доброго імені. Вибачте, але TryParse - це не моя ідея хорошого імені.

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

IList<Point> Intersection(Line a, Line b) { ... }

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

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

Maybe<Point> Intersection(Line a, Line b) { ... }

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

Я знаю, це дивно. Але це новий інструмент (ну, для нас). Тому дайте трохи часу. Молотки мають більше сенсу, коли ви перестаєте їх використовувати на саморізах.


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

Чому жодна з відповідей не пояснює, що монада Ефіра не є кодом помилки, і не є OneOf? Вони принципово різні, і, отже, питання, як видається, засноване на непорозумінні. (Хоча в модифікованій формі, це все-таки є актуальним питанням.) - Конрад Рудольф 4 червня 18 о 14:08

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


9
Чи не було б більш доречним, якби червона доріжка опинилася під полями, таким чином не пробігаючи їх?
Flater

4
Логічно, ви маєте рацію. Однак кожне поле в цій конкретній діаграмі являє собою монадичну операцію прив'язки. bind включає (можливо, створює!) червону доріжку. Рекомендую переглянути повну презентацію. Це цікаво, і Скотт - чудовий оратор.
Гусдор

7
Я думаю, що винятки більше схожі на інструкцію COMEFROM, а не на GOTO, оскільки throwнасправді не знає / не скажемо, куди ми підскочимо;)
Warbo

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

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

35

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

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

Це не є загальновизнаною істиною. Вибір у кожній мові, яка підтримує винятки, - це або викинути виняток (який абонент не може обробити), або повернути деяке складене значення (яке ОБОВ'ЯЗКОВО ОБОВ'ЯЗКОВО).

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


9
Розмножувати монади нічого не потрібно.
Василевс

23
@Basilevs Це потребує підтримки для поширення монад, зберігаючи код читабельним, якого C # не має. Ви або отримуєте повторні інструкції, якщо повертаєтесь, або ланцюжки лямбда.
Себастьян Редл

4
@SebastianRedl лямбдаси добре виглядають на C #.
Василевс

@Basilevs З точки зору синтаксису, так, але я підозрюю, що з точки зору продуктивності вони можуть бути менш привабливими.
Фарап

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

26

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

Із цим завжди були винятки у формі TryXXXметодів, які б повернули booleanуспішний результат та забезпечили результат другим значенням через outпараметр. Вони дуже схожі на шаблони "спробувати" у функціональному програмуванні, за винятком того, що результат надходить через вихідний параметр, а не через Maybe<T>зворотне значення.

Але все змінюється. Функціональне програмування стає ще популярнішим, і такі мови, як C #, реагують. C # 7.0 ввів деякі основні функції узгодження шаблону до мови; C # 8 запровадить набагато більше, включаючи вираз комутації, рекурсивні структури тощо. Поряд з цим, спостерігається зростання "функціональних бібліотек" для C #, таких як моя власна бібліотека Succinc <T> , яка також підтримує дискриміновані спілки. .

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

static Maybe<int> TryParseInt(string source) =>
    int.TryParse(source, out var result) ? new Some<int>(result) : none; 

public int GetNumberFromUser()
{
    Console.WriteLine("Please enter a number");
    while (true)
    {
        var userInput = Console.ReadLine();
        if (TryParseInt(userInput) is int value)
        {
            return value;
        }
        Console.WriteLine("That's not a valid number. Please try again");
    }
}

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

Ми ще там не говоримо " НЕ повертайте коди помилок". є застарілим і потребує звільнення. Але марш по дорозі до цієї мети вже ведеться. Тож візьміть свій вибір: дотримуйтесь старих способів кидання винятків із відмовою від геїв, якщо це так, як вам подобається робити справи; або почніть вивчати новий світ, де конвенції з функціональних мов стають все більш популярними на C #.


22
Ось чому я ненавиджу C # :) Вони постійно додають шляхи до шкіри кота, і, звичайно, старі способи ніколи не відходять. Зрештою, немає нічого умовного, програміст-перфекціоніст може годинами сидіти, вирішуючи, який метод "приємніший", і новий програміст повинен навчитися всьому, перш ніж зможе прочитати код інших.
Олександр Дубінський

24
@AleksandrDubinsky, Ви потрапили на головну проблему з мовами програмування взагалі, а не лише на C #. Створюються нові мови, худорляві, свіжі та сповнені сучасних ідей. І дуже мало людей користується ними. З часом вони завойовують користувачів та нові функції, прикріплені до старих функцій, які неможливо видалити, оскільки це порушить існуючий код. Наростає набряк. Тож народні люди створюють нові мови, худорляві, свіжі та сповнені сучасних ідей ...
Девід Арно

7
@AleksandrDubinsky ти не ненавидиш це, коли вони додають тепер функції з 1960-х.
ctrl-alt-delor

6
@AleksandrDubinsky Так. Оскільки Java спробувала додати щось колись (генерики), вони не змогли, тепер нам доведеться жити з жахливим безладом, який спричинив, щоб вони засвоїли урок і кинули додавати щось взагалі
:)

3
@Agent_L: Ви знаєте, що додані Java java.util.Stream(наполовину схожий IEnumerable-подібний) і лямбда, зараз? :)
cHao

5

Я думаю, що цілком розумно використовувати DU для повернення помилок, але тоді я б сказав, що, як я писав OneOf :) Але я б сказав, що все одно, оскільки це звичайна практика у F # тощо (наприклад, тип результату, як згадували інші ).

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

Ось приклад зі сторінки проекту.

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

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

Наскільки OneOf є ідіоматичним у c #, це вже інше питання. Синтаксис є дійсним та досить інтуїтивним. ДУ та орієнтоване на залізничне програмування добре відомі поняття.

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


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

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

4

OneOfприблизно еквівалентні перевіреним виняткам на Java. Є деякі відмінності:

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

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

OneOf і перевірений виняток мають спільну важливу "особливість":

  • вони обоє змушують вас обробляти їх скрізь у стеку викликів

Це запобігає вашому страху

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

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

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


+1, але чому OneOf не працює з побічними ефектами? (Я думаю, це тому, що він пройде перевірку, якщо ви не призначите значення повернення, але, як я не знаю OneOf, ні багато C # Я не впевнений - уточнення покращить вашу відповідь.)
dcorking

1
Ви можете повернути інформацію про помилку з OneOf, зробивши повертається об'єкт помилки містить корисну інформацію , наприклад , OneOf<SomeSuccess, SomeErrorInfo>де SomeErrorInfoнадає докладну інформацію про помилку.
mcintyre321

@dcorking Відредаговано. Звичайно, якщо ви проігнорували повернене значення, то нічого не знаєте про те, що сталася проблема. Якщо ви робите це з чистою функцією, це добре (ви просто витратили час).
maaartinus

2
@ mcintyre321 Безумовно, ви можете додавати інформацію, але це потрібно робити вручну, і це підлягає лінові та забуванню. Гадаю, у більш складних випадках корисніший слід стека.
maaartinus

4

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

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

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

Помилки, які не можна запобігти, часто і виправляти - це ті, з якими насправді потрібно вирішити на сайті виклику. Вони найкраще виправдовують примушення абонента протистояти їм. У Java я роблю їх перевіреними винятками, і подібний ефект досягається, роблячи їх Try*методами або повертаючи дискримінаційний союз. Дискримінаційні спілки особливо приємні, коли функція має повернене значення. Вони є значно більш досконалою версією повернення null. Синтаксис try/catchблоків не великий (занадто багато дужок), завдяки чому альтернативи виглядають краще. Крім того, винятки трохи повільні, оскільки вони фіксують сліди стека.

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

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

На моєму досвіді, змушувати абонента протистояти умовам помилок, це добре, коли помилки не можна запобігти, часто і виправляти , але це стає дуже дратує, коли абонент змушений стрибати через обручі, коли вони не потребують або хочуть . Це може бути тому, що абонент знає, що помилка не відбудеться, або тому, що вони (ще) не хочуть зробити код стійким. У Java це пояснює протилежність частому використанню перевірених винятків та поверненню Необов’язкового (що схоже на "Можливо" та нещодавно було запроваджено на тлі багато суперечок). Коли ви сумніваєтеся, киньте винятки і дозвольте абоненту вирішити, як вони хочуть поводитися з ними.

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


2

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

OneOf - це не код помилки, він більше схожий на монаду

Спосіб його використання OneOf<Value, Error1, Error2>- це контейнер, який або представляє фактичний результат, або деякий стан помилки. Це як Optional, за винятком того, що коли немає значення, він може надати більше деталей, чому це.

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

Єдина проблема - коли ти не піклується про результат. Приклад з документації OneOf дає:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username) { ... }

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

public void CreateUserButtonClick(String username) {
    UserManager.CreateUser(string username)
}

то вам дійсно є проблеми і виключення буде кращим вибором. Порівняти рейл saveVS save!.


1

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

Причина, по якій MSDN каже вам "не повертати коди помилок", саме в цьому - ніхто не змушує вас навіть читати код помилки. Компілятор вам зовсім не допомагає. Однак якщо ваша функція не має побічних ефектів, ви можете сміливо використовувати щось на кшталт Either- справа в тому, що навіть якщо ви ігноруєте результат помилки (якщо такий є), ви можете це робити, лише якщо ви не використовуєте "належний результат" або. Існує кілька способів, як це досягти - наприклад, ви можете дозволити "читати" значення лише, передавши делегата для обробки випадків успіху та помилок, і ви можете легко поєднувати це з підходами, такими як програмування, орієнтоване на залізницю (це робить працювати просто чудово в C #).

int.TryParseдесь посередині. Це чисто. Він визначає, якими є значення двох результатів у будь-який час - тож ви знаєте, що якщо значення повернення falseбуде, буде встановлено вихідний параметр 0. Це все ще не заважає вам використовувати вихідний параметр, не перевіряючи значення повернення, але принаймні вам гарантовано, який результат буде, навіть якщо функція не працює.

Але одна абсолютно важлива річ тут - послідовність . Компілятор не збирається економити вас, якщо хтось змінить цю функцію на побічні ефекти. Або кинути винятки. Тому, хоча цей підхід є ідеальним для C # (і я його використовую), вам потрібно переконатися, що всі у вашій команді розуміють і використовують його . Багато інваріантів, необхідних для того, щоб функціональне програмування працювало чудово, не виконується компілятором C #, і вам потрібно переконатися, що за ними дотримуєтесь самостійно. Ви повинні бути в курсі того, що порушується, якщо ви не дотримуєтесь правил, які Haskell виконує за замовчуванням - і це те, що ви пам’ятаєте для будь-яких функціональних парадигм, які ви вводите у свій код.


0

Правильне твердження було б: Не використовувати коди помилок для виключень! І не використовуйте винятки для невиключень.

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

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

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


7
Я принципово не погоджуюся з передумовою цієї відповіді. Якщо дизайнери мови не мали наміру використовувати виключення для виправлення помилок, catchключове слово не існувало б. А оскільки ми говоримо в контексті C #, то варто відзначити, що філософія, яку підтримують тут, не дотримується рамки .NET; мабуть, найпростіший уявний приклад "недійсного введення, з яким ви можете обробити", - це користувач, який вводить не-число в поле, де очікується число ..., і int.Parseкидає виняток.
Марк Амері

@MarkAmery Для int.Parse - це нічого, з чого він не може відновитись, і нічого, чого він би очікував. Застереження про вилов зазвичай використовується для уникнення повного відмови з боку зовнішнього виду, воно не запитує у користувача нових облікових даних бази даних тощо, це дозволить уникнути збоїв програми. Це технічний збій, а не потік управління додатком.
Френк Хопкінс

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

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

-2

Це "жахлива ідея".

Це жахливо, оскільки, принаймні, у .net, у вас немає вичерпного списку можливих винятків. Будь-який код, можливо, може кинути будь-який виняток. Особливо, якщо ви робите OOP, і ви можете викликати замінені методи на підтип, а не на оголошений тип

Тож вам доведеться змінити всі методи та функції OneOf<Exception, ReturnType>, тоді, якщо ви хочете обробляти різні типи винятків, вам доведеться вивчити тип If(exception is FileNotFoundException)тощо

try catch є еквівалентом OneOf<Exception, ReturnType>

Редагувати ----

Я знаю, що ви пропонуєте повернутися, OneOf<ErrorEnum, ReturnType>але я вважаю, що це дещо відмінність без різниці.

Ви поєднуєте коди повернення, очікувані винятки та розгалуження за винятком. Але загальний ефект той самий.


2
"Звичайні" Винятки - теж жахлива ідея. підказка у назві
Еван

4
Я не пропоную повернути Exceptions.
Клінтон

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