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

Комментарии 36

По поводу исключений ещё один интересный вопрос — производительность. try-catch вместо if — это плохо не только потому, что ухудшает читабельность кода, а ещё и потому, что это просто медленнее.
А вот это заявление дискуссионное. В каком-нибудь enterprise приложении боятся бросить разовое исключение из-за потери производительности как минимум опрометчиво. Да и производительность аппаратуры сейчас позволяет пренебречь этим фактором в угоду скорости разработки.
Это медленнее можно заметить, только если вы будете выполнять миллионы if'ов или бросать миллионы исключений в секунду. В остальных «реальных» ситуациях этой разницей можно пренебречь. С подобным же энтузиазмом можно говорить про замедление времени на вызов одной функции из другой или про замедление вызова виртуального метода по сравнению с обычным. А потом смотришь — в коде сплошной boxing/unboxing, а они исключение боятся бросить. Ответ тут один — профилируйте код, и вам откроется очень много интересного (то, что тормозит совсем не там, где вы думаете).
if'ов или бросать миллионы исключений в секунду.

Нет, дело не в «бросании», а в самой постановке обработчика, если это, например, Windows SEH обработка. Но да — заметно только в циклах.
Постановка обработчика SEH — это всего лишь две операции со стеком. Снятие обработчика — еще две.

Дорогой является именно процедура возбуждения исключения, включающая в себя системный вызов и еще кучу нетривиальных действий.
Да, но их могло и не быть. Комментарий, с которого началась ветка, содержит
try-catch вместо if — это плохо не только потому, что ухудшает читабельность кода, а ещё и потому, что это просто медленнее.

Заметьте, сказано не о создании исключения, а о блоке. Мой комментарий написан именно в этом контексте — «if против try..catch», а не «try..catch затратнее/менее затратен, чем throw»

Дорогой является именно процедура возбуждения исключения, включающая в себя системный вызов

Это если исключение «честное» и проходит через ядро.
Исключать цену throw из цены try/catch сравнивая try/catch и if — некорректно. Ведь в гипотетической программе, написанной с использованием первой конструкции, конструкция throw также будет присутствовать — ведь без нее try/catch не сможет заменить if, в альтернативной же программе, построенной на if-ах, конструкции throw быть не может.
Я не исключал, вы что-то путаете. Это была ремарка про «нормальное» выполнение без исключений и сопутствующие накладные расходы. А так — каждому случаю — свой инструмент.
Проблема затронута правильная — отсутствие информации об исключениях в сигнатуре метода. Вот только решение — отказаться от почти всех исключений — странное. Ведь если проблема в отсутствии информации, то ее можно туда добавить!

Можно добавить информацию об исключении в xmldoc. А можно — начать название метода с префикса ThrowIf — тогда факт возможного «выпадения» исключения будет виден еще лучше.
В Java, вроде, пока не пропишешь в сигнатуре, не можешь наружу пробросить исключение. Но там тоже от этого какие-то неудобства, если не ошибаюсь.
А ThrowIf мне видится половинчатым решением. Что если внутренний метод бросает исключение, но мы пока не знаем как его обрабатывать, тоже помечать ThrowIf? Но это будет вводить в заблуждение.
В Java есть два типа исключений:
— декларируемые, которые нужно декларировать по всей иерархии вызовов
— RuntimeException, которые появились из-за диких проблем с первыми. Они работают так же, как исключения в С#
А какие именно эти дикие проблемы? погуглил. Одна из основных, как я понял, лежит в плоскости подавления исключений где-то по середине, дабы ублажить компилятор.
Основной проблемой первых считается то, что изменение имплементации функции может потребовать изменение её сигнатуры, что не совсем верно
// Это баг или метод специально был помещен сюда без try/catch блока?
ValidateName(name);

Какой-то искусственный пример. Вполне возможно в корпоративной документации есть мануал о том как сообщать об ошибках и возможно там есть строчки вида «Мы всегда бросаем исключения». Если члены команды между собою договорились то и проблемы нет! А новичку всегда нужно давать читать «курс молодого бойца»
Проблема документации в поддержании актуальности документов и синхронизации, в случае изменения, со всеми членами команды. Человеческий фактор довольно быстро превращает красивую модель с использованием документации в еще одну проблему (как на той самой картинке).

Да и наименьшего удивления приведен не просто так — код по возможности должен быть самоописываемым. Да, это очень тяжело реализовать в реальных условиях, но жить с этим гораздо легче.
Проблема документации в поддержании актуальности документов и синхронизации

Проблем с поддержанием нет.

Приведу пример: Как только мне придет в голову светлая мысль и я закомичу код в котором в качестве отступа будет табуляция вместо 4 пробелов в компании, где работаю, Заверяю Вас мои коллеги мне сразу же дадут знать о том как надо и как не надо писать код.

Правила нужны не для того чтобы туда записать и потом их поддерживать. Правила нужны для того чтобы избавлять команду от проблем! Если это правило действительно приносит серьезную пользу множеству людей и причем существенную, то правило быстро будет ВСЕГДА применяться на практике. Человеку просто не дадут сделать неправильно.

>>Да и наименьшего удивления приведен не просто так
Это имеет смысл, если Вы пришли в сторонний проект, к примеру в Open-Source. Тогда да, лучше никого не удивлять и явно говорить о том что Вы хотите выразить. Но в компаниях лучше выстраивать правила с другими членами команды. Код будет проще. Понятнее.
Забавно, когда заходишь почитать комментарии и видишь те же самые доводы и те же самые контр-аргументы, 1 в 1. В прошлый раз я отстаивал вашу позицию.
В прошлый раз я отстаивал вашу позицию.

Ваши слова звучат так, как будто бы Ваша позиция поменялась. Это так? Если да, то почему? ;)
Нет, все то же. Просто забавно, когда аргументы повторяются слово в слово. И каждый раз — как вновь.
Вот то, что я рассказал бы про исключения тому, кто пока не умеет ими пользоваться.

  • Исключения нужно использовать тогда и только тогда, когда возникает развитие событий, не предусмотренное нормальным ходом работы приложения — исключительной ситуации. При этом причина может быть как статической (например, логическая ошибка в коде), так и динамическая (например, недоступность ресурсов).
  • Исключения нужно кидать максимально точно (узко) типизированными.
  • Исключения замечательны для решения своей задачи — прерывания процесса с информированием о возникшей проблеме — причине прерывания, потому что они всплывают по стеку до нужного места. Для других задач они не подходят.
  • Обрабатывать исключения нужно там, где их одновременно возможно и уместно обработать.
  • В прикладном ПО большинство бросаемых на практике исключений не обрабатываются, перехватываются в самой высокой точке стека и попадают в лог, а пользователь получает ошибку 500 «Что-то пошло не так».
НЛО прилетело и опубликовало эту надпись здесь
Ага, а потом утром придя на работу, а служба поддержки пользователей встречает тебя с вилами:
— Нам всё утро звонили пользователи и просили объяснить им «Что именно пошло не так, и что им с этим делать?»
Я каждый день работаю с разработчиками, и всё больше и больше убеждаюсь, что вопросы диагностики и работы с исключениями разработчиками спускаются на тормозах. Почему? Это вопрос философский, чем практический.

При работе с исключениями, я бы выделила две особенности:
а. Исключения для разработчика
б. Исключения для пользователя

Во-первых, сообщения для разработчика нужно снабжать необходимой _полезной_ информацией. Что бы те получив исключение UserNotFoundException долго не мучились пытаясь хоть как-то понять, какой именно пользователь не был найден. И всегда помнить, что исключение может произойти тогда, когда ваше приложение развёрнуто в 100500 серверах по такому же количеству ДЦ и подключится отладчиком, или включить подробную (verbose) детализацию логов у вас может не получится.

Ко второй категории, исключений я бы отнесла те, что имеют все больше шансов быть отображёнными пользователю. (Это ведь исключения. Никто не может дать 100% гарантии.) Такие исключения, нужно не только чётко типизировать, но ещё и давать им идентификационные номера, классы, описание, и всё, что будет вам полезно услышать от пользователя, в момент его обращения к вам с проблемой.

N.B. А многие любят писать такое:
try
{
}
catch(Exception ex)
{
log.Write(...., ex);
}

По этому мы на собеседования выносим вопросы:
а. А как себя поведёт этот код, если случилось StackOverflowException? SEHException? OutOfMemoryException?

P.S. Спасибо разработчикам C# 6.0 за фильтрацию исключений

Exception Filters
Исключения нужно использовать тогда и только тогда, когда возникает развитие событий, не предусмотренное нормальным ходом работы

Увы, в любой строке кода может произойти «не нормальный ход работы». Деление на ноль, целочисленное переполнение, ошибка выделения памяти, ошибка отсутствия прав доступа к ресурсу (файлу и т.п.), невозможность выполнения операции (например, передача данных по закрытому сокету и т.п.). Т.е. всегда нужно писать код с мыслью в голове, что из любой строки кода может прийти исключение. И пусть даже его обработка будет заключаться в «перехвате в самой высокой точке стека», все равно это гораздо лучше, чем где-то в глубине загасить исключение и делать вид, что всё хорошо. Иначе получится не программа, а «труп прибитый гвоздями к стене» (с) Джоэл Спольски.
Тема обработки исключений — одна из важнейших в девелоперстве. А вот статья слабенькая, особенно для перевода.

Даже нет предмета для обсуждений.

Хороший вопрос для обсуждения: нужны ли исключения бизнес-процесса. К примеру, если при попытке перевода средств оказалось что на счету недостаточно денег — выбрасывать исключение с деталями ошибки (нет денег/счет заблокирован и пр.) или возвращать класс, содержащий информацию об ошибке?
Ответ псевдокодом (субъективно):

function transferMoney(value, fromWalet, toWalet) {
  lock(fromWalet); // throws CantAquireLockException after timeout
  try {
    if (!fromWalet.hasFunds(value)) {
      throw new InsufficientFundsException(fromWalet, value);
    }
    try {
      SomeTransactionalSystem.begin();
      fromWalet.addFunds(-value);
      toWalet.addFunds(value);
      SomeTransactionalSystem.commit();
    } catch (Exception e) {
      SomeTransactionalSystem.rollback();
      throw e;
    }
  } catch (Exception e) {
    unlock(fromWalet);
    throw e;
  }
  unlock(fromWalet);
}


Таким образом transferMoney становится полностью самостоятельной процедурой, которая делает свою работу, ничего не возвращает и кидает исключения, когда не может ее сделать. Она не нуждается в блокировках на уровне выше. При этом никто не мешает на уроне выше повторить проверку fromWalet.hasFunds(value), что бы не доводить до исключения. Просто это будет вне блокировки и при параллельной работе с одним fromWalet может возникнуть ситуация, когда InsufficientFundsException все же вылетит.

или возвращать класс, содержащий информацию об ошибке?

Возвращать что угодно, содержащее сообщение об ошибке, должен только метод на подобии getError или getLastError. Есть 2 варианта — проверка (валидация) (никаких исключений и возврат true/false) и выполнение (в случае ошибки — исключение). В данном случае я бы предпочел сочетание обоих, т.е. сначала проверить fromWalet.funds < value, потом вызвать transferMoney. Проверка прямо скажет true/false
Этот код будет феерически работать на больших нагрузках. Распределенные транзакции (а кто сказал, что у вас счета физически на одной машине) — это очень дорого, блокировки каждого счета — тоже.
Этот код будет отлично работать на больших нагрузках. Потому что по одному счету не будет даже 100 обращений в секунду (если будет, то это становится центральным моментом проектирования и код нужно писать иначе, а здесь в примере — это погрешность — это код для 98% биллингов, но не для высокочастотного трейдинга). А заявление о том, что что-то дорого само по себе — ни о чем. Дорого или дешево может быть только относительно.

А спорить о транзакциях и блокировках с вами я больше не буду — это долго и бессмысленно. Проходили, знаем.
А заявление о том, что что-то дорого само по себе — ни о чем. Дорого или дешево может быть только относительно.

Это (распределенные транзакции и блокировки) дороже (с точки зрения ресурсов), чем неблокирующие системы.
Интересный способ пиара собственного блога. Написать на английском, а потом разместить как перевод.
Эта статья — отличная иллюстрация, почему в Go нет и не нужны исключения: их заменяет явный возврат ошибки с их явной обработкой, и паники в случае действительно исключительных ситуаций, требующих раскручивания стека; причём при работе с библиотеками так же удобнее работать с возвращаемыми оттуда ошибками, а не исключениями.
Это кстати не только в Go такая тенденция. В функциональных языках тоже заметно стремление использовать контейнеры с результатом выполнения операции вместо выбрасывания исключения. Either монада как пример
Во всех примерах там возвращается error вместо конкретной ошибки. Из-за этого не ясно какие ошибки может вернуть метод. Вот пример:
golang.org/pkg/errors
Из которого выносится простое правило: если событие происходит редко, следует использовать исключения, если часто — программные проверки. Собственно, это более широкая трактовка о том, что исключения нужны в исключительных ситуациях.
Ну а если покритиковать подход Scala, обернуть исключение в монаду, тогда мы можем знать, что попытались выполнить операцию, но возможно у нас не получилось. Кто ее распутывать будет, тот и обработает это исключение. Минус тут вижу, что в отличии от нормальной функции, тут throw уподобляется return, т.е быстрый goto в конец метода.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории