Навіщо ловити та повторно викидати виняток у C #?


557

Я переглядаю статтю C # - Об'єкт передачі даних про серіалізаційні DTO.

У статтю включений цей фрагмент коду:

public static string SerializeDTO(DTO dto) {
    try {
        XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
        StringWriter sWriter = new StringWriter();
        xmlSer.Serialize(sWriter, dto);
        return sWriter.ToString();
    }
    catch(Exception ex) {
        throw ex;
    }
}

Інша частина статті виглядає розумною та розумною (для noob), але цей спробу-ловля кидає WtfException ... Хіба це не зовсім рівно, ніж взагалі не обробляти винятки?

Ergo:

public static string SerializeDTO(DTO dto) {
    XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
    StringWriter sWriter = new StringWriter();
    xmlSer.Serialize(sWriter, dto);
    return sWriter.ToString();
}

Або я пропускаю щось принципове щодо обробки помилок у C #? Це майже те саме, що і Java (мінус перевірені винятки), чи не так? ... Тобто вони обоє вдосконалювали C ++.

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


Редагувати:

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

НЕ

try {
    // Do stuff that might throw an exception
}
catch (Exception e) {
    throw e; // This destroys the strack trace information!
}

Інформація про стеження стека може мати вирішальне значення для виявлення першопричини проблеми!

ДО

try {
    // Do stuff that might throw an exception
}
catch (SqlException e) {
    // Log it
    if (e.ErrorCode != NO_ROW_ERROR) { // filter out NoDataFound.
        // Do special cleanup, like maybe closing the "dirty" database connection.
        throw; // This preserves the stack trace
    }
}
catch (IOException e) {
    // Log it
    throw;
}
catch (Exception e) {
    // Log it
    throw new DAOException("Excrement occurred", e); // wrapped & chained exceptions (just like java).
}
finally {
    // Normal clean goes here (like closing open files).
}

Ловіть більш конкретні винятки перед менш конкретними (як і Java).


Список літератури:


8
Хороший підсумок; додаткові бали за включення остаточного блоку.
Фредрік Морк

Я хотів би додати, що ви можете використовувати "кинути"; бути ще кориснішим, додавши параметри, які були надіслані методу в колекції e.Data перед "кидком"; заява
Майкл Багіг

@MickTheWarMachineDesigner (та художник за сумісництвом). Так? Ви говорите про обробку винятків Microshite Suckwell (можливо, з 2005 року на сьогодні, наскільки я знаю). Я говорив про обробку винятків взагалі. І так, я дізнався дещо, коли я опублікував цю БІЛЬШИЙ ЧЕТВОРОДЖЕННЯ РОКІВ .... Але так, я визнаю, що ви маєте справжню точку, але я вважаю, що ви пропустили реальну точку; якщо ви отримаєте мій дрейф? Це питання стосується генералізованої обробки винятків у C #; а точніше про викидання винятків ... ВСІХ видів. Класно?
corlettk

Спробуйте перенести розділ підсумків редагування у запитанні до власної відповіді. Чомусь див. " Редагування самовідповіді" поза сумнівом та " Відповідь", вбудований у питання .
DavidRR

2
Хтось не помітив частину "Екскреція відбулася"? це здається, що код пішов на пуп!
Джейсон Локі Сміт

Відповіді:


430

Спочатку; те, що код у статті робить це, є злим. throw exскине стек викликів за винятком того пункту, де знаходиться цей оператор кидка; втрачаючи інформацію про те, де фактично створено виняток.

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

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

try 
{
    // code that may throw exceptions    
}
catch(Exception ex) 
{
    // add error logging here
    throw;
}

6
@Fredrick, просто fyi (хоча ви, мабуть, знаєте), якщо ви не збираєтесь використовувати цей exоб'єкт, тоді його не потрібно інстанціювати.
Еойн Кемпбелл

78
@Eoin: Якщо його не буде створено миттєво, це буде досить важко записати його.
Сем Топор

30
Так, я думаю, що "зло" - це правильно ... розглянемо випадок нульового вказівника, викинутого кудись із великого коду. Повідомлення - ванільне, без сліду стека, який вам залишився, "щось десь було нульовим". НЕ добре, коли виробництво мертве; і у вас немає НІ хвилини або менше, щоб вирішити проблему з полум'ям, і відмовитись або виправити її ... Хороша обробка винятків - це вагу золота.
corlettk

4
Це правда і для Java ... "кинути" проти "кинути екс"?
JasonStoltz

8
@Jason, дивіться це питання . У Java throw exне перезапускають стек-трек.
Метью Флашен

117

Не робіть цього,

try 
{
...
}
catch(Exception ex)
{
   throw ex;
}

Ви втратите інформацію про стежку стека ...

Або робити,

try { ... }
catch { throw; }

АБО

try { ... }
catch (Exception ex)
{
    throw new Exception("My Custom Error Message", ex);
}

Однією з причин, яку ви можете захопити, є, якщо ви обробляєте різні винятки, наприклад

try
{
   ...
}
catch(SQLException sex)
{
   //Do Custom Logging 
   //Don't throw exception - swallow it here
}
catch(OtherException oex)
{
   //Do something else
   throw new WrappedException("Other Exception occured");
}
catch
{
   System.Diagnostics.Debug.WriteLine("Eeep! an error, not to worry, will be handled higher up the call stack");
   throw; //Chuck everything else back up the stack
}

7
Чому б просто не залишити спійманий викид?
AnthonyWJones

3
Ще залишається цінністю у виході улов {кидок; } внизу списку конкретних виловів типу винятків із тих підстав, що це доводить, що автор розглянув випадок, хоча коментар може бути однаково достатнім. Не здогадуючись, коли ви читаєте код, це добре.
annakata

87
Чомусь назва SQLException мене турбує.
Майкл Майерс

13
Цей вилов (виняток) {кидайте новий виняток (...)} - це те, що ви ніколи не повинні робити ніколи , просто тому, що ви забруднюєте інформацію про винятки та робите фільтр винятків у подальшому над стеком викликів зайвим. Єдиний раз, коли ви повинні зловити один тип винятку і кинути інший, це коли ви реалізуєте рівень абстракції, і вам потрібно перетворити тип виключення для провайдера (наприклад, SqlException проти XmlException) в більш загальний (наприклад, DataLoadingException).
варення

3
@dark_perfect, ви повинні перевірити цей аргумент вперед, на початку методу і кинути туди ArgumentNullException (невдало швидко).
Андрій Бозантан

56

C # (до C # 6) не підтримує CIL "відфільтровані винятки", що робить VB, тому в C # 1-5 однією з причин повторного викидання винятків є те, що у вас не вистачає інформації під час лову () щоб визначити, чи хотіли ви насправді зловити виняток.

Наприклад, у VB можна зробити

Try
 ..
Catch Ex As MyException When Ex.ErrorCode = 123
 .. 
End Try

... який не обробляє MyExceptions з різними значеннями ErrorCode. У C # до версії v6 вам доведеться спіймати і перекинути MyException, якщо ErrorCode не було 123:

try 
{
   ...
}
catch(MyException ex)
{
    if (ex.ErrorCode != 123) throw;
    ...
}

Оскільки C # 6.0 ви можете фільтрувати так само, як і за допомогою VB:

try 
{
  // Do stuff
} 
catch (Exception e) when (e.ErrorCode == 123456) // filter
{
  // Handle, other exceptions will be left alone and bubble up
}

2
Дейв, але (як мінімум у Java) ти б не кидав "загальний" MyException, ти визначив би СПЕЦИФІЧНИЙ тип винятку та кинув його, що дозволить його диференціювати за типом у блоці вилову ... Але так , якщо ваш не архітектор винятку (я думаю, що JDBC SQLException (знову ж таки Java) тут, що огидно загальне, і викриває метод getErrorCode () ... Хммм ... Ви маєте бачення, це просто так Я думаю, що є кращий спосіб зробити це, де це можливо. Ура, Мате, я дуже ціную ваш час. Кіт.
corlettk

1
Ну, питання "Чому виловлювати та повторно вигнати виняток у C #?", І це відповідь. =] ... і навіть за спеціалізованими винятками, Фільтри винятку мають сенс: розгляньте випадок, коли ви, скажімо, обробляєте SqlTimeoutException і SqlConnectionResetException, які є і SqlException. Фільтри винятків дозволяють вам зловити SqlException лише тоді, коли він є одним із цих двох, тому замість того, щоб захаращувати ваш спробу / ловити однаковою обробкою для цих двох, ви могли б "зловити SqlException ex, коли ex є SqlTimeoutException Або ex SqlConnectionResetException". (Я не Дейв btw)
bzlm

3
Відфільтровані винятки надходять у C # 6!
Сер Кріспалот

14

Моя основна причина наявності коду типу:

try
{
    //Some code
}
catch (Exception e)
{
    throw;
}

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

Хоча вони приємні під час налагодження.


1
Так, я заплачу за це, але так, ви б не хотіли бачити це в опублікованому коді ... ergo: Мені було б соромно публікувати його ;-)
corlettk

25
Насправді, це не обов’язково - у Visual Studio ви можете встановити налагоджувальний пристрій, щоб він був порушений, коли викидається виняток, і він відображає деталі винятків у вікні інспектора для вас.
jammycakes

8
Якщо ви хочете використовувати якийсь код ТІЛЬКИ під час налагодження, використовуйте #if DEBUG ... #endif, і вам не потрібно буде видаляти ці рядки
Michael Freidgeim

1
Так, я це робив кілька разів сам. Раз у раз втече до звільнення. @jammycakes Проблема з перервою Visual Studio на виняток полягає в тому, що іноді я хочу, щоб викид не був єдиним (або навіть лише одним з його типів). Досі не знаю умови перерви з "перервою, якщо пропущено за винятком". До цього часу це стане корисним. Майкл Freidgeim: #if DEBUGнавколо ОБИДВА try {і } catch () {...}трохи брудний і, чесно кажучи, робить мене нудило ... Попередньо процесор, взагалі кажучи, не мій друг.

11

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

public static string SerializeDTO(DTO dto) {
  try {
      XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
      StringWriter sWriter = new StringWriter();
      xmlSer.Serialize(sWriter, dto);
      return sWriter.ToString();
  }
  catch(Exception ex) {
    string message = 
      String.Format("Something went wrong serializing DTO {0}", DTO);
    throw new MyLibraryException(message, ex);
  }
}

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

10

Хіба це зовсім не рівно, ніж взагалі не обробляти винятки?

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


8

Ви не хочете кидати колишнього - оскільки це втратить стек викликів. Див. Розділ Обробка винятків (MSDN).

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


Ви не втрачаєте весь стек викликів, використовуючи функцію кидання ex, ви просто втрачаєте частину стека викликів з того місця, коли виняток стався вище його стека викликів. Але ви зберігаєте стек викликів із методу, який викинув виняток туди, куди його закликав клієнт. Насправді можуть бути випадки використання, коли ви користуєтесь цим, інакше хороші люди в Microsoft не допустили б цього. Це сказав, що я не використовував це. Ще одне питання, яке слід пам’ятати, - кидати винятки дорого. Робіть це лише з дуже виправданої причини. Я б міг вважати виправданим лісозаготівлю тощо.
Чарльз Оуен

5

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

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

У більшості мов .NET єдиний спосіб вчинити дії коду, заснованого на винятку, - catchце йому (хоча він знає, що виняток не вирішує), виконайте відповідну дію та повторно throw). Ще один можливий підхід, якщо коду не важливо, який викид буде кинуто, - використовувати okпрапор із try/finallyблоком; встановіть okпрапор falseперед блоком і trueперед виходом блоку та перед тим, returnщо знаходиться в блоці. Тоді в межах finally, припустимо, що якщо okце не встановлено, повинен статися виняток. Такий підхід семантично кращий, ніж a catch/ throw, але некрасивий і менш доцільний, ніж повинен бути.


5

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

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

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


3

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


Я не отримав те, про що ти говорив, поки не прочитав посилання ... і я все ще не зовсім впевнений, про що ти йдеш ... мені зовсім не знайомий з VB.NET. Я думаю, що це призводить до того, що сума повідомляється про "непослідовну", правда? ... Я ВЕЛИКИЙ шанувальник статичних методів .. окрім того, що вони прості, менше шансів на непослідовність, якщо відокремити налаштування атрибутів від коду що робить фактичну роботу. Стек - "самоочищення".
corlettk

3
Люди очікують, що коли вони напишуть "спробуйте {Foo ();} нарешті {Bar ();}", між Foo і Bar нічого не проходить. Але це неправда; якщо ваш абонент додав фільтр винятків, і немає втрученого 'catch', і Foo () кидає, то якийсь інший випадковий код від вашого абонента запуститься до того, як нарешті (Bar) запуститься. Це дуже погано, якщо ви зламали інваріанти або підвищили рівень безпеки, очікуючи, що вони нарешті будуть "негайно" відновлені до норми і жоден інший код не побачить тимчасових змін.
Брайан

3

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

Ви можете сказати, Catch io.FileNotFoundExeption exа потім використати альтернативний шлях до файлу чи якийсь подібний, але все-таки киньте помилку.

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


3

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

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

Для цього ви можете згорнути весь код у спробу / catch / нарешті. Нарешті встановіть курсор назад до правого курсору. Щоб ви не ховали жодних дійсних винятків, закиньте їх у вилов.

try
{
    Cursor.Current = Cursors.WaitCursor;
    // Test something
    if (testResult) return;
    // Do something else
}
catch
{
    throw;
}
finally
{
     Cursor.Current = Cursors.Default;
}

1
Це catchбуло обов'язковою частиною в try...finallyісторичному відношенні чи грає функціональну роль у цьому прикладі? - Я просто перевірив двічі і взагалі можу користуватися try {} finally {}без блоку лову.
Себі

2

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

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

Повторне повторення, хоча немає жодного сенсу ловити виняток у прикладі, який ви розмістили. НЕ роби це так!


1

Вибачте, але багато прикладів як "покращений дизайн" все ще жахливо пахнуть або можуть бути вкрай оманливими. Спробувавши {} catch {log; кидок} просто безглуздо. Журнал винятків слід проводити в центральному місці всередині програми. винятки все-таки перекидають стек-трек, чому б не записувати їх десь і близько до меж системи?

Необхідно використовувати обережність, коли ви серіалізуєте свій контекст (тобто DTO в одному з наведених прикладів) просто в повідомлення журналу. Він може легко містити конфіденційну інформацію, яка, можливо, не захоче потрапити до рук усіх людей, які мають доступ до файлів журналу. І якщо ви не додасте до винятків будь-якої нової інформації, я дійсно не бачу сенсу перетворювати винятки. У доброї старої Java є певний сенс для цього, вона вимагає, щоб абонент знав, які винятки слід очікувати, а потім викликати код. Оскільки у вас немає цього в .NET, обгортання не приносить користі принаймні 80% випадків, які я бачив.


Дякую за вашу думку, Джо. У Java (і, напевно, C #) я хотів би бачити анотацію на рівні класу @FaultBoundary, яка змушує ВСІ винятки (включаючи неперевірені типи винятків) бути схопленими або оголошеними для кидання. Я б використав цю примітку на публічних інтерфейсах кожного архітектурного шару. Тож інтерфейс @FaultBoundary ThingDAO не міг би просочити деталі реалізації, такі як SQLExceptions, NPE або AIOB. Натомість "причинно-наслідкова" стека буде записана, і DAOSystemException буде кинуто ... Я визначаю виняток системи як "постійно фатальний".
corlettk

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

1
Тут зручно властивість Data класу Exception - фіксує всю локальну інформацію для загального ведення журналу. Ця стаття спочатку донесла мою увагу: blog.abodit.com/2010/03/…
McGuireV10

1

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


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

@HamzaLH, я погоджуюся, що це не добре написана відповідь, але вона має інформацію, відмінну від інших відповідей та позитивні голоси. Тож я не розумію, чому ви пропонуєте її видалити? "Короткі відповіді, які є темою і дають рішення, все ще відповідають". Із сайту meta.stackexchange.com/questions/226258/…
Michael Freidgeim

це лише відповідь на посилання
LHIOUI

1. Відповіді лише на посилання мають бути змінені на коментарі, а не видалені. 2. Це посилання на інше питання ПС, а не на зовнішній сайт, який вважається менш імовірним порушеним у часі. 3. У ньому є додатковий опис, який робить його не "лише посиланням" - див. Meta.stackexchange.com/questions/225370/…
Michael Freidgeim

1

Більшість відповідей говорять про сценарій catch-log-rethrow.

Замість того, щоб писати його у своєму коді, подумайте про використання AOP, зокрема Postsharp.Diagnostic.Toolkit з OnExceptionOptions IncludeParameterValue і IncludeThisArgument


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

@TonyDong, я погоджуюся, що це не добре написана відповідь, але вона має інформацію, відмінну від інших відповідей та позитивні голоси. Тож я не розумію, чому ви пропонуєте її видалити? До речі, посилання через 5 років все ще діє. "Короткі відповіді, які є темою і дають рішення, все ще відповідають". З meta.stackexchange.com/questions/226258/…
Michael Freidgeim

Stackoverflow має лише цю пропозицію.
Тоні Донг

@TonyDong, якщо відповідь не є абсолютно марною, виберіть "Виглядає добре"
Michael Freidgeim

0

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

Приклад:

string numberText = "";
try
{
    Console.Write("Enter an integer: ");
    numberText = Console.ReadLine();
    var result = int.Parse(numberText);

    Console.WriteLine("You entered {0}", result);
}
catch (FormatException)
{
    if (numberText.ToLowerInvariant() == "nothing")
    {
        Console.WriteLine("Please, please don't be lazy and enter a valid number next time.");
    }
    else
    {
        throw;
    }
}    
finally
{
    Console.WriteLine("Freed some resources.");
}
Console.ReadKey();

Однак існує й інший спосіб зробити це, використовуючи умовні пропозиції у блоках улов:

string numberText = "";
try
{
    Console.Write("Enter an integer: ");
    numberText = Console.ReadLine();
    var result = int.Parse(numberText);

    Console.WriteLine("You entered {0}", result);
}
catch (FormatException) when (numberText.ToLowerInvariant() == "nothing")
{
    Console.WriteLine("Please, please don't be lazy and enter a valid number next time.");
}    
finally
{
    Console.WriteLine("Freed some resources.");
}
Console.ReadKey();

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

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