Страницы

Поиск по вопросам

среда, 27 ноября 2019 г.

Грамотное использование try-catch

#c_sharp #исключения #try_catch


Вычитал, что использование try-catch весьма ресурсоёмко. Так ли это?

Если так, то как следует грамотно использовать try-catch?

Теоретически ведь можно практически всё делать по старинке, через коды возврата,
минуя использование try-catch, но тогда код пострадает в плане читаемости из-за громоздких
последовательностей if или switch. 
    


Ответы

Ответ 1



Для начала определите для себя, что такое «ресурсоёмко». Любой вопрос производительности стоит рассматривать в контексте с производительностью всей остальной программы. Если у вас алгоритм на целочисленной арифметике, то в нём try/catch может заметно замедлять производительность (а может и не замедлять). Если у вас идёт запись в файлы или обращение к базе данных, то обработка исключения будет на порядки быстрее остальных операций, поэтому нет никакого смысла её оптимизировать. Обычно try/catch нисколько не замедляет программу, если исключений фактически нет. То есть кидая исключение только в случае ошибки, вы не потеряете в производительности, когда ошибок нет. Если же ошибка произошла, то работа вашего алгоритма всё равно будет прервана, поэтому не так важно, что вы один раз потеряете дополнительно пару микросекунд (вряд ли больше) перед выдачей ошибки пользователю. Вам стоит беспокоиться, только если вы в цикле кидаете миллион исключений, потом их ловите, игнорируете и переходите на следующую итерацию. Но такой код как раз уже попахивает: получается, вы используете исключения для управления потоком вычислений, а не для информирования об исключительной ситуации. Но даже в такой ситуации JIT-компилятор может сообразить. Если в пределах заинлайненного кода он увидит и выбрасывание исключения и поимку и заметит, что при поимке вы игнорируете исключение (в частности, не читаете стектрейс из него), то он потенциально может заменить выбрасывание исключения на обычный goto, не создавая объект исключения вообще. Вообще обычно самое медленное в исключении — это создание стектрейса. Общее правило: пишите код так, как рекомендуют авторы языка и признанные специалисты в данной области. Если производительность кода вас не устраивает, профилируйте его, находите узкие места и оптимизируйте именно их (возможно, отступая от красоты кода в угоду скорости). Но не оптимизируйте то, что занимает полпроцента от общего времени выполнения. Не надо оптимизировать преждевременно.

Ответ 2



Applications and libraries should not use return codes to communicate errors. Вы подходите к проблеме не с той стороны. Настоящая выбор - это не "использовать ли try/catch или код возврата". А, скорее "выбросить исключение или вернуть код". Есть пара основных правил: исключения не должны использоваться для нормального control flow. исключения в коде должны всегда обозначать ошибку - исключительную ситуацию - что-то пошло не так, как планировалось. в случае ошибки нужно всегда использовать использовать исключение, а не код возврата. Все сводится к определению "как планировалось" и "что ожидалось", "произошла ли ошибка, или все идет нормально" в конкретном контексте. Вот пара примеров: Разработчик вызывает File.OpenRead. Вряд ли он ожидает, что файла нет, или что для открытия не хватит прав. Поэтому File.OpenRead должен бросить исключение (что он и делает). Разработчик достает значение из кэша - вызовом Page.Cache["news"]. Ожидает ли он, что значения в кэше не окажется - скорее всего да, причем даже в случае если 5 минут назад значение в кэше было. Чего он точно не ожидает - так это исключения и необходимости оборачивать обращения к кэшу в try/catch. Ловить ли исключения? Зависит от того, что вы собираетесь с ними делать. Есть всего несколько возможных причин ловить исключения: Ваш код может восстановиться после исключения - нормально продолжить работу. Это достаточно редкая ситуация. Обычно восстановление сводится к попытке повторить упавший вызов - и применимо оно только у удаленным сервисам, вроде баз данных. Типичный пример - ретрай команды к SQL Server при получении дедлока. Ваш код может добавить ценные данные к исключению - дописать что-то, что облегчит дальнейшую отладку. В таком случае исключение надо поймать и бросить новое исключение со старым в качестве InnerException. Ваш код - это код верхнего уровня, например, UI или основной метод worker-а. Тогда в нем стоит поймать исключение, записать его в лог и показать пользователю красивое сообщение об ошибке (если пользователь есть). Во всех остальных случаях никакого смысла ловить исключения нет. Все это достаточно подробно расписано в MSDN, Design Guidelines for Exceptions

Ответ 3



Это справедливо только в теории, или в экстремальных случаях вроде циклов с огромным количеством итераций. Можно придумать еще ряд примеров, где использование try будет нежелательно из-за медлительности, но все это - достаточно редкие случаи, в большинстве своем означающие, что либо автор кода пишет очень плохой код, который нуждается в оптимизации и/или рефакторинге, либо хочет использовать c# для каких-то чересчур чувствительных в плане производителности задач, и тогда, вероятно, C# и управляемый код - не лучший выбор. А вот в 99% случаев опасения по поводу "медлительности" try/catch - это экономия на спичках и добровольное лишение себя возможности писать легко поддерживаемый код (все эти мучения с кодами возврата способны принести немало головной боли). Маленький эксперимент. Выполните у себя вот этот код static void SomeAction() { var rand = new Random(); for (int i = 0; i < 1000; i++) rand.Next(); } static void Handle() { // просто заглушка } static void WithException(int iterations) { var watch = Stopwatch.StartNew(); try { for (int i = 0; i < iterations; i++) SomeAction(); throw new Exception(); } catch (Exception e) { Handle(); } watch.Stop(); NumberFormatInfo nfi = (NumberFormatInfo) CultureInfo.InvariantCulture.NumberFormat.Clone(); nfi.NumberGroupSeparator = " "; Console.WriteLine("with exception {1} iteration {0} ms", watch.ElapsedMilliseconds, iterations.ToString("n0", nfi)); Console.WriteLine(); } static void WithoutException(int iterations) { var watch = Stopwatch.StartNew(); for (int i = 0; i < iterations; i++) SomeAction(); Handle(); watch.Stop(); NumberFormatInfo nfi = (NumberFormatInfo) CultureInfo.InvariantCulture.NumberFormat.Clone(); nfi.NumberGroupSeparator = " "; Console.WriteLine("without exception {1} iteration {0} ms", watch.ElapsedMilliseconds, iterations.ToString("n0", nfi)); } static void Main(string[] args) { WithoutException(1); // этот и следующий вызов для "прогрева" WithException(1); WithoutException(1); WithException(1); WithoutException(1000); WithException(1000); WithoutException(100000); WithException(100000); WithoutException(500000); WithException(500000); Console.ReadLine(); } и посмотрите результаты (можете попробовать выполнить его прямо на ideone, но он скорее всего отвалится с таймаутом из-за большого количества итераций) Мои результаты были таковы: Как можно видеть, время выполнения кода, выбрасывающего и обрабатывающего исключение ничтожно отличается от кода без исключений. А вот удобство обработки ошибок на порядок выше. Конечно, тест кустарный и далеко не идеален, но уверяю вас, в 99% случаев проблема производительности выбрасывания и обработки исключений не стоит выеденного яйца.

Ответ 4



Когда говорят о ресурсоёмкости, надо понимать, по сравнению с чем. Если мы сравниваем с одним сложением — try/catch более затратен. Если мы сравниваем с чтением байта из файла — намного менее затратен. Исходя из этого, давайте разобьём вашу программу на части. UI. Когда мы говорим о UI, в основном время теряется на ожидании ввода/реакции пользователя. Это время на несколько порядков больше, чем выброс/обработка исключения. Поэтому использование исключений в UI-коде — нормальная вещь. Уровень бизнес-логики, контроллеры, вью-модели и такое прочее. Flow программы представляет собой не более чем одно новое состояние в секунду — иначе пользователь просто не успеет разобраться, что же произошло. Это значит, что для управления flow прекрасно могут применяться исключения. То же касается кода, коммуницирующего с UI. Модели. Вот здесь, конечно, выбрасывание исключений может оказаться невыгодным. В любом случае, вам нужно посмотреть на то, чем занимается данная модель, и как часто возникают ошибочные ситуации. Если это чтение файла, или чтение данных из интернета, то опять-таки накладные расходы на коммуникацию с внешним миром настолько велики, что нет смысла экономить на спичках. А вот если узкое место вашей модели — процессорное время (например, это числомолотилка), то здесь уже время, затраченное на обработку исключения, может оказаться сравнимым с типичным временем обработки элемента, и значит, придётся внимательнее планировать стратегию реакции на ошибки. В этом случае, однако, даже если вы будете применять коды ошибок внутри модели, имеет смысл сообщать о суммарной ошибке наверх именно при помощи исключения. В любом случае, я бы предостерёг вас от преждевременной оптимизации. В подавляющем большинстве случаев узким местом будет вовсе не обработка исключений. Поэтому пишите ваш код с исключениями, и если вас не удовлетворяет производительность, проведите профилирование. Только оно сможет сказать вам, что же реально нужно улучшить. Если окажется, что проблема в исключениях, лишь тогда есть смысл избавляться от них.

Комментариев нет:

Отправить комментарий