Как стать автором
Обновить

Никто не умеет обрабатывать ошибки

Время на прочтение9 мин
Количество просмотров113K
Из одной книги в другую, из статьи в статью кочует мнение о том, что выражение

try {
   //do something
}
catch(Exception ex) {
}

является плохой практикой. Возврат кодов – также плохая практика. Но становится ли нам, программистам, жить легче с этими знаниями и так уж ли они неоспоримы? И самый забавный вопрос – кто-нибудь в мире умеет грамотно обрабатывать ошибки, возникающие по ходу работы приложения? (под этим я понимаю обработку только тех ошибок, которые имеет смысл обрабатывать и вывод сообщений об ошибках, которые соответствуют действительно произошедшей, которые не вводят пользователя в замешательство, а в идеале и предлагают решение возникшей проблемы).

Целью данной статьи не является «разнос» существующих концепций, за незнанием автором лучшего подхода. Целью статьи является заострение проблемы обработки исключений и признание того факта, что недостаточно просто делать отписки в стиле «не делайте catch(Exception)» или «бросайте всегда исключения наверх – авось, там, наверху разберутся». Это ничем не помогает. На русском языке на эту тему так вообще мало чего написано (да и кто доверяет русскоязычным авторам? Шутка, но в каждой шутке есть доля шутки). Статья предназначена также для не умудрённых опытом разработчиков, чтобы читатель осознал то, что мучаются обработкой ошибок абсолютно все, включая «сильнейших мира сего», а также начал понимать проблему на немного более глубоком уровне.

Для начала хотелось бы представить мой перевод двух статей:
1. Проблемы с проверяемыми исключениями. Беседа с Андерсом Хейлсбергом. Часть 2. 18 августа 2003.
2. Жизнь с непроверяемыми исключениями. 3 марта 2014.

Хотелось бы также упомянуть, что в целях краткости я не переводил абсолютно всё из этих статей. Это вырезки, которые являются наиболее важными с точки зрения заданной темы. Текст от себя я буду обрамлять следующим образом: Spock speech текст end of Spock speech

Проблемы с проверяемыми исключениями.


Bruce Eckel: C# не имеет механизма проверяемых исключений. Каким образом принималось решение о том включать или нет механизм проверяемых исключений в C#?
Anders Hejlsberg: Я вижу две проблемы с проверяемыми исключениями: масштабируемость и версионируемость.
Bruce Eckel: Раньше я думал, что проверяемые исключения — это круто.
Anders Hejlsberg: Именно. Честно говоря, при поверхностном рассмотрении, они действительно выглядят хорошо и кажется, что с этой идеей всё нормально. Я абсолютно согласен с тем, что механизм исключений это отличная фича. Просто конкретная реализация может быть проблематичной. Если реализовать этот механизм так, как он был реализован в Java, то, я думаю, вы просто размениваете один набор проблем на другой. В результате для меня неясно становится ли жизнь хоть сколько-нибудь легче. Мы просто делаем жизнь другой.
Bruce Eckel: Были ли в команде разработчиков C# разногласия относительно проверяемых исключений?
Anders Hejlsberg: Нет, я думаю, что в нашей команде проектировщиков языка в большей степени царило согласие. C# молчалив по отношению к проверяемым исключениям. Если однажды лучшее решение будет найдено – и поверьте мне, мы продолжаем думать о данной проблеме – мы сможем вернуться и прикрутить, что нужно. Я убеждённый сторонник того, что если вам нечего сказать такого, что продвинет искусство вперёд, то лучше промолчать и придерживаться нейтральной позиции, а не пытаться создать свой фреймворк.
Bruce Eckel: Разработчики, практикующие экстремальное программирование говорят «делай наиболее просто, чтобы работало».
Anders Hejlsberg: Да, Эйнштейн сказал «делай наиболее просто, но не проще простого». Полагаю, что проверяемые исключения это наручники для программистов. Можно наблюдать программистов, которые берутся использовать какой-нибудь новый API, декларирующий множество потенциально пробрасываемых исключений и можно увидеть насколько засорённым становится их код. В результате вы осознаёте, что проверяемые исключения им ничем не помогают.
Bill Venners: Вы упоминали масштабируемость и версионируемость относительно проверяемых исключений. Не могли бы вы пояснить, что вы имели ввиду под двумя проблемами, связанными с этими понятиями?
Anders Hejlsberg: Начнём с версионирования, потому что тут проблемы легко увидеть. Скажем я создаю метод foo, который декларирует проброс исключений A, B и C. Во второй версии метода я хочу добавить пару фишек и теперь метод foo может выкинуть также исключение D. Добавление нового исключения является несовместимым изменением, потому что существующие пользователи этого метода почти 100% не будут ловить это исключение.
Добавление нового исключения ломает клиентский код. Это как добавление метода в интерфейс.
Bill Venners: Но не ломаете ли вы клиентский код в любом случае, даже если в языке отсутствуют проверяемые исключения?
Anders Hejlsberg: Нет, потому что в большом количестве случаев людям «по-барабану». Они не собираются ловить какие-либо из этих исключений. На нижнем уровне, вокруг цикла сообщений находится обработчик исключений. Этот обработчик просто выводит окошко о том, что что-то пошло не так. Программисты защищают свой код, покрывая его повсюду конструкциями try\finally, так что они просто уклоняются от обработки исключения, именно исключение они обрабатывать и не собирались.
Bill Venners: В общем, вы считаете, что в наиболее распространённом случае пользователи предпочитают обработчик на вершине стека, явной обработке исключений?
Anders Hejlsberg: Забавно, что люди думают, что наиболее важной частью взаимоотношений с исключениями является их обработка. Это как раз не так важно. В хорошо написанном приложении отношение конструкций try\finally к try\catch примерно 10 к 1.
Bill Venners: Так что же в результате?
Anders Hejlsberg: В результате, вы защищаете себя от исключений, а не обрабатываете их. Обработку исключений вы реализуете где-то в другом месте. Естественно, что в любом типе событийно-ориентированного приложения, как в случае с любым современным UI-приложением, вы реализуете обработку исключений вокруг цикла сообщений и просто здесь их и обрабатываете. Но по ходу программы вы защищаете себя, освобождая выделенные ресурсы, которые были захвачены и так далее. Вы подчищаете за собой так, что вы всегда находитесь в непротиворечивом состоянии. Вы не хотите писать программу, которая в сотне разных мест обрабатывает исключения и выбрасывает окна с сообщениями об ошибках.
Обработка исключений должна быть централизованной и вы должны просто защищать себя в то время как исключения распространяются до обработчика.

Spock speech
Проблема с масштабируемостью не меняет смысла обработки исключений так, как было предложено Андерсом Хейлсбергом, поэтому перевод выкладывать с точки зрения данной темы не целесообразно (кому интересно – могут пройти и посмотреть, но в целом всё сводится к тому, что при росте программы, количество выбрасываемых исключений растёт и никто не сможет их все обрабатывать, особенно методы, которые могут пробросить десяток-другой различных типов исключений, ваши обработчики превратятся в адские портянки try\catch и код станет невозможно поддерживать).
Совсем недавно, 3 марта 2014 года Эрик Липперт в своём блоге также поднял тему обработки исключений в C#.
Обсуждение он разбил на две части: в первой части он задал несколько вопросов, а во второй части агрегировал ответы и сделал резюме.
Итак, вопросы, которые задал читателям своего блога Эрик:
end of Spock speech

Жизнь с непроверяемыми исключениями


  • Имели ли ваши программы баги, которые вы исправили, добавив обработку исключения, о возможности выброса которого вы даже не подозревали?
  • Когда вы пишете новый код, который вызывает методы, которые вы не разрабатывали, то каким образом вы догадываетесь о том, какие исключения могут быть выброшены? Существуют ли типы исключений, которые, по вашему мнению, должны быть всегда обработаны в независимости от того, какой метод их выбрасывает? Например, верно ли то, что любой метод, выбрасывающий IOException должен быть всегда завёрнут в try-catch?


Spock speech
Был ещё задан 3-й вопрос, но вопрос не очень интересный и ответы на него также особого интереса не представляют, поэтому соответствующие части будут опущены.
end of Spock speech

Основной вывод по комментариям читателей: исключения привносят небольшой беспорядок в C#. Семантика языка и организация (или недостаток организации) иерархии исключений делает сложным узнать то, какие исключения нужно ловить и какие следует пропустить. Множество читателей оставили множество отличных комментариев, но один из них произвёл на меня наиболее сильное впечатление:
Я думаю, что вся концепция «обработки» исключений слегка напоминает игру для дураков. Я, наверное, могу посчитать на пальцах одной руки количество случаев, когда я был действительно в состоянии обработать специфический тип исключения и сделать в обработчике что-то интеллектуальное. В 99% случаев ты должен ловить или всё или ничего. Когда выбрасывается исключение любого типа, восстановите стабильное состояние и затем либо продолжайте, либо прерывайте исполнение программы.

Это грубо, но, я думаю, справедливо.
Этот комментарий предполагает Покемон-обработку – поймай их всех! (gotta catch ‘em all) – вот решение проблемы. Я был удивлён тем, что почти треть комментаторов выразили поддержку использования catch(Exception), ведь исторически это описывалось как плохая практика компанией Микрософт. C#, как я обычно говорю, спроектирован быть «кабинкой успеха» («pit of success»), где то, что является наиболее простым, является и наиболее правильным. Эта замечательная цель, похоже, в данном случае не была достигнута. Если catch(Exception) это и наименее верный путь и наиболее простой, то это потому что правильный путь слишком тяжёл.
Подавляющее большинство комментаторов написали, что правили баги, причиной которых было отсутствие catch для конкретных типов исключений, хотя такие случаи у разных людей случались с разной периодичность: «от 1 до 2 раз», «изредка», до «часто».
Треть сказали, что они использовали MSDN и другие формы документации (включая XML-комментарии) в целях определения типов исключений, которые следовало бы ловить. MSDN и хвалили и критиковали; какие-то части MSDN написаны отлично, другие написаны так, что ничего не понятно. Третью часть документации всесторонне изкритиковали, такой документации никто не верит.
Четверть сказали, что использовали что-то вроде метода проб и ошибок – отладка, тестирование, чтение логов, получения краш-дампов для того, чтобы выяснить какие исключения следует ловить.
Опять же, разочарование было резюмировано следующим комментарием:
Каждый try/catch блок это упражнение в разочаровании, потому что ты думаешь, что ловишь нужный тип исключения до тех пор, пока всё не сломается в эксплуатации.
Достаточно много комментаторов заметили, что система обработки исключений предполагает наиболее важной информацией сам тип исключения, однако одного типа недостаточно для того, чтобы произвести правильную обработку исключения.

Spock speech
Внезапно! Почти всё, что сказано либо прямо, либо косвенно противоречит The Best Practice!
Подкину ещё проблему: что, если вам нужно в цикле вызывать код, который всё время выбрасывает исключения в случае, если что-то не так? Старая непроизводительная машина (а их есть у нас в России великое множество) «сдохнет». Тут придут на помощь (если вы владелец вызываемого метода) коды возвратов или экземпляры классов, которые содержат в себе нужную информацию. Ой, оказывается коды возвратов не мертвы, как ожидалось.
А от catch(Exception) вам никуда не деться: вам придётся повсюду, а не только на уровне цикла сообщений делать catch(Exception), если только вы не считаете, что в вашей конкретной ситуации пусть себе исключения летают через всю систему (чем дальше летят, тем больше жрут ресурсов).
Остаётся одна загвоздка с catch(Exception): мы можем поймать StackOverflow или OutOfMemory и даже глазом не повести, что приведёт к печальным последствиям, за которые можно заплатить миллионами рублей (или долларов), если вы не пишете Hello, World!
Для «решения» (намеренно взял в кавычки, поскольку едва ли существующие решения проблем с обработкой ошибок кого-либо полностью удовлетворяют, или приближаются хотя бы близко к полному удовлетворению) означенной проблемы подойдёт фильтрование. Кстати говоря, несмотря на то, что в MSDN фильтрация исключений признаётся не лучшей практикой, сама часть Enterprise Framework ответственная за обработку исключений базируется на фильтровании, которое настраивается через соответствующие политики, сюрприз!
Вот простой статический класс, упрощающий обработку исключений:
public static class Exceptions {
        private static readonly List<Type> fatalExceptions = new List<Type> {
            typeof (OutOfMemoryException),
            typeof (StackOverflowException),
//Ещё типы исключений, который по вашему мнению всегда являются фатальными
        };

        public static string FullMessage(this Exception ex) {
            var builder = new StringBuilder();
            while (ex != null) {
                builder.AppendFormat("{0}{1}", ex, Environment.NewLine);
                ex = ex.InnerException;
            }
            return builder.ToString();
        }

        public static void TryFilterCatch(Action tryAction, Func<Exception, bool> isRecoverPossible, Action handlerAction) {
            try {
                tryAction();
            } catch (Exception ex) {
                if (!isRecoverPossible(ex)) throw;
                handlerAction();
            }
        }

        public static void TryFilterCatch(Action tryAction, Func<Exception, bool> isRecoverPossible, Action<Exception> handlerAction) {
            try {
                tryAction();
            } catch (Exception ex) {
                if (!isRecoverPossible(ex))
                    throw;
                handlerAction(ex);
            }
        }

        public static bool NotFatal(this Exception ex) {
            return fatalExceptions.All(curFatal => ex.GetType() != curFatal);
        }

        public static bool IsFatal(this Exception ex) {
            return !NotFatal(ex);
        }
    }

Примеры использования:
Exceptions.TryFilterCatch(host.Close, Exceptions.NotFatal,
                    ex => logger.Error("Исключение при закрытии хоста сервиса.", ex));

 private bool TryGetTpmProcess(out Process process) {
            process = null;
            try {
                process = Process.GetProcessById(App.TpmProcessId.GetValueOrDefault());
                return true;
            } catch (Exception ex) {
                if (ex.IsFatal())
                    throw;
                return false;
            }
        }


Метод TryFilterCatch позволяет «портянки» преобразовывать в краткую запись. Методы расширения также оказываются удобными в использовании. Способ с TryFilterCatch подсмотрел здесь.

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

Так что, не верьте в «утверждения вылитые в граните», смотрите в оба и не позволяйте исключениям убивать ваше приложение.

end of Spock speech
Теги:
Хабы:
+48
Комментарии121

Публикации

Истории

Работа

.NET разработчик
74 вакансии

Ближайшие события