Чи є законні причини повернення об’єктів виключення замість того, щоб їх кидати?


45

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

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

Питання: Чи існують законні ситуації, коли винятки не кидаються та не потрапляють, а просто повертаються із методу, а потім передаються як об’єкти помилок?

Це питання виникло для мене, оскільки System.IObserver<T>.OnErrorметод .NET 4 пропонує саме таке: винятки передаються навколо як об'єкти помилок.

Давайте розглянемо інший сценарій, валідація. Скажімо, я дотримуюся загальноприйнятої мудрості, і тому я розрізняю тип об'єкта помилки IValidationErrorта окремий тип винятку, ValidationExceptionякий використовується для повідомлення про несподівані помилки:

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

( System.Component.DataAnnotationsПростір імен робить щось зовсім подібне.)

Ці види можуть бути використані наступним чином:

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

Тепер мені цікаво, чи не можу я спростити вищезазначене до цього:

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

Питання: Які переваги та недоліки цих двох різних підходів?


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

2
@Bobson: Я вважаю, що ці два питання є доповнюючими: попереднє питання повинно визначати загальний контекст випуску в широкому розумінні, а друге запитує конкретну інформацію, яка повинна дати змогу читачам зробити власний висновок. Відповідь не повинна давати окремих, чітких відповідей на обидва питання; відповіді "між ними" так само вітаються.
stakx

5
Мені щось незрозуміло: в чому причина отримання ValidationError із Exception, якщо ви не збираєтесь його кидати?
пдр

@pdr: Ви маєте на увазі випадок накидання AggregateException? Влучне зауваження.
stakx

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

Відповіді:


31

Чи існують законні ситуації, коли винятки не кидаються та не потрапляють, а просто повертаються із методу і потім передаються навколо як об'єкти помилок?

Якщо його ніколи не кидають, то це не виняток. Це об'єкт derivedіз класу «Виняток», хоча він не слідкує за поведінкою. Називати це виключенням є суто семантикою на даний момент, але я не бачу нічого поганого в тому, щоб не кидати його. З моєї точки зору, не кидати виняток - це саме те саме, що і внутрішня функція, яка охоплює її і не дозволяє їй поширюватися.

Чи дійсно функція повертати об'єкти виключення?

Абсолютно. Ось короткий список прикладів, коли це може бути доречно:

  • Виняток фабрика.
  • Об'єкт контексту, який повідомляє про попередню помилку як виняток, готовий до використання.
  • Функція, яка зберігає раніше вилучений виняток.
  • Сторонній API, який створює виняток внутрішнього типу.

Чи не кидаєш це погано?

Метання винятком виглядає на кшталт: "Ідіть до в'язниці. Не минайте Го!" в настільній грі Монополія. Він повідомляє компілятору перейти через увесь вихідний код до виходу, не виконуючи жодного з цього вихідного коду. Це не має нічого спільного з помилками, повідомленнями або зупинками помилок. Мені подобається думати про викид як вираз "супер повернення" для функції. Він повертає виконання коду кудись набагато вище, ніж початковий абонент.

Тут важливо зрозуміти, що справжня цінність винятків полягає в try/catchшаблоні, а не в інстанції об'єкта винятку. Об'єктом винятку є лише повідомлення про стан.

У вашому запитанні, здається, ви плутаєте використання цих двох речей: перехід до обробника винятку та стан помилки, який представляє виняток. Тільки те, що ви взяли стан помилки та перетворили його на виняток, не означає, що ви слідуєте за try/catchшаблоном або його перевагами.


4
"Якщо його ніколи не кидають, то це не виняток. Це об'єкт, похідний від Exceptionкласу, але не слідкує за поведінкою." - І саме в цьому полягає проблема: чи законно використовувати Exceptionоб'єкт, що отримується - у випадку, коли він взагалі не підкинеться ні виробником об'єкта, ні будь-яким методом виклику; де воно служить лише як "повідомлення стану", яке передається навколо, без контрольного потоку "супер повернення"? Або я порушую невизначений try/ catch(Exception)такий контракт настільки сильно, що ніколи не повинен цього робити, а замість цього використовувати окремий, нетиповий Exception?
stakx

10
Програмісти @stakx мають і продовжуватимуть робити те, що бентежить інших програмістів, але технічно невірно. Тому я сказав, що це семантика. Юридична відповідь - Noви не порушуєте жодних контрактів. Було popular opinionб те, що ти цього не повинен робити. Програмування наповнене прикладами, коли ваш вихідний код не робить нічого поганого, але всі це не люблять. Вихідний код завжди повинен бути записаний таким чином, щоб було зрозуміло, у чому полягає наміри. Ви справді допомагаєте іншому програмісту, використовуючи виняток таким чином? Якщо так, то зробіть так. Інакше не дивуйтеся, якщо це не подобається.
Реакційний

@cgTag Використання блоків вбудованого коду для курсиву робить мій мозок боляче ..
Фрейт

51

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

Одним з можливих випадків використання може бути функція, яка перетворює коди відповідей HTTP у винятки:

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

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


Це також допомагає компілятору зрозуміти потік коду: якщо метод ніколи не повертається правильно (тому що він видає "бажаний" виняток) і компілятор не знає про це, то іноді вам можуть знадобитися зайві returnзаяви, щоб компілятор перестав скаржитися.
Йоахім Зауер

@JoachimSauer Так. Я не раз хотів би, щоб вони додавали до C # повернення типу "ніколи" - це вказувало б, що функція повинна вийти, кинувши, вклавши в неї повернення - це помилка. Іноді у вас є код, єдине завдання якого є винятком.
Лорен Печтел

2
@LorenPechtel Функція, яка ніколи не повертається, не є функцією. Це термінатор. Виклик такої функції, що очищає стек виклику. Це схоже на дзвінки System.Exit(). Код після виклику функції не виконується. Це не схоже на гарну модель дизайну для функції.
Реакційний

5
@MathewFoscarini Розглянемо клас, який має кілька методів, які можуть помилитися подібними способами. Ви хочете скинути деяку інформацію про державу як частину винятку, щоб вказати, що вона жувала, коли вона була. Ви хочете отримати одну копію примірного коду через DRY. Це залишає або виклик функції, яка виконує підготування та використання результату у вашому киданні, або означає функцію, яка будує попередньо оброблений текст, а потім кидає його. Я взагалі вважаю останнього вищим.
Лорен Печтел

1
@LorenPechtel ах, так, я теж це зробив. Гаразд я зараз розумію.
Реакційний

5

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

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

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


Я не думаю, що жодні помітні люди виступають за таку різницю щодо моделі "Спробуйте", але "рекомендований" підхід повернення булевого виглядає досить анемічно. Я думаю, що може бути краще мати абстрактний OperationResultклас із boolвластивістю "Успішно", GetExceptionIfAnyметодом та AssertSuccessfulметодом (який би викинув виняток з, GetExceptionIfAnyякщо недійсний). Наявність GetExceptionIfAnyметоду, а не властивості, дозволить використовувати статичні незмінні об'єкти помилок у випадках, коли сказати було не так багато корисного.
supercat

5

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

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

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

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

Взагалі, я б тримався осторонь від створення винятку і ніколи не кидав його (якщо тільки оптимізація продуктивності після профілювання не зазначає це як вузьке місце). Принаймні, у java, де створення виключень дороге (слід стека). У C # це можливо, але ІМО його дивує і тому слід уникати.


Це часто трапляється і з об'єктами слухача помилок. Черга виконає завдання, вловить будь-яке виняток, а потім передасть цей виняток відповідальному слухачеві помилок. Зазвичай це не має сенсу вбивати нитку завдань, яка не знає багато про роботу в цьому випадку. Така ідея також вбудована в API Java, де один потік може передавати винятки з іншого: docs.oracle.com/javase/1.5.0/docs/api/java/lang/…
Lance Nanek

1

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

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

У цьому випадку ми помилимось і зупинимо обробку файлу, як тільки він виявив погану запис.

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

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

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

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


1

Питання: Чи існують законні ситуації, коли винятки не кидаються та не потрапляють, а просто повертаються з методу, а потім передаються як об’єкти помилок?

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

Ілюстративний код:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function

1

У більшості мов (afaik) винятки мають деякі додаткові смаколики. Здебільшого, в об’єкті зберігається знімок поточного сліду стека. Це добре для налагодження, але також може мати наслідки для пам'яті.

Як вже було сказано @ThinkingMedia, ви справді використовуєте виняток як об’єкт помилки.

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

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

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

Отже, це гарна ідея?

Поки що я не бачив, щоб його застосовували чи рекомендували. Але це мало значить.

Питання полягає в тому: чи дійсно корисний слід стека для обробки помилок? Або взагалі, яку інформацію ви хочете надати розробнику чи адміністратору?

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


-4

Відповідь - так. Див. Http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions та відкрий стрілку для посібника зі стилю C ++ Google про винятки. Ви можете бачити аргументи, проти яких вони вирішували, але кажуть, що якби їм довелося це зробити заново, вони, швидше за все, вирішили.

Однак варто зазначити, що мова Go не ідіоматично використовує винятки з подібних причин.


1
Вибачте, але я не розумію, як ці вказівки допомагають відповісти на питання: Вони просто рекомендують повністю уникати поводження з винятками. Про це я не прошу; моє питання полягає в тому, чи є законним використовувати тип винятку для опису стану помилки в ситуації, коли отриманий об’єкт винятку ніколи не буде кинутий. У цьому керівництві про це, мабуть, нічого немає.
stakx
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.