исключения
Aug. 28th, 2012 04:18 pmЗашла тут в http://plumqqz.livejournal.com/306627.html речь об исключениях. Что навело меня на размышления и формулировку того, чего мне не хватает для того, чтобы их нормально использовать. Нужны блоки типа таких:
exok {
// здесь всякие вызываемые функции могут бросаться исключениями
};
Вне таких блоков или все исключения должны молча (или опционально не молча, а с записью на stderr) посылаться нафиг, или же компилятор должен проверять, что никто не может ничего исключать (явный оператор throw - может). Вариант - те же блоки с явным указанием разрешенных типов исключений:
exok (ExcType1, ..., ExcTypeN) {
};
В-принципе, вместо exok можно нормально задействовать try. К которому опционально можно добавить catch-finally, а можно не добавлять, если все и без того нормально.
Смысл в том, чтобы все места появления исключений можно было отловить при локальном чтении куска кода, не лазя на десять уровней вглубь каждой вызываемой функции.
exok {
// здесь всякие вызываемые функции могут бросаться исключениями
};
Вне таких блоков или все исключения должны молча (или опционально не молча, а с записью на stderr) посылаться нафиг, или же компилятор должен проверять, что никто не может ничего исключать (явный оператор throw - может). Вариант - те же блоки с явным указанием разрешенных типов исключений:
exok (ExcType1, ..., ExcTypeN) {
};
В-принципе, вместо exok можно нормально задействовать try. К которому опционально можно добавить catch-finally, а можно не добавлять, если все и без того нормально.
Смысл в том, чтобы все места появления исключений можно было отловить при локальном чтении куска кода, не лазя на десять уровней вглубь каждой вызываемой функции.
no subject
Date: 2012-08-28 10:20 pm (UTC)все исключения должны молча (или опционально не молча, а с записью на stderr) посылаться нафиг,
Такой метод программирования называется Old maid (Schwarzer Peter - нем, или Акулина на русском). Все ошибки надёжно прячутся, а в дураках оказывается тот, кто не может их передать дальше. (Как правило, это пользователь)
no subject
Date: 2012-08-29 02:31 pm (UTC)no subject
Date: 2012-08-29 05:15 pm (UTC)Если сервер будет тихо под одеялом жрать говно, на костыли и подпорки уйдёт много человеколет.
no subject
Date: 2012-08-29 05:54 pm (UTC)no subject
Date: 2012-08-29 08:07 pm (UTC)Потому что неизвестно, какой где вызываемой фигне отчего вздумается бросить исключения.
Пардон, исключения для того и придуманы, чтобы подробно и чётко именно об этом и рапортовать.
no subject
Date: 2012-08-30 01:51 pm (UTC)no subject
Date: 2012-08-30 02:14 pm (UTC)Где, кто и почему?
no subject
Date: 2012-08-30 02:58 pm (UTC)1. Программе - это тип исключения.
2. Пользователю - это человеко-читаемое сообщение.
3. Программисту - это трассировка стека. Как вариант - вместо бросания исключения делать core dump, чтобы можно было искать подробности кривизны, которые теряются при раскручивании стека.
И с раскручиванием этой информации происходят всякие странные вещи. Ну, не говоря уже о том, что она может просто отсутствовать: например, в C++-ных исключениях трассировки стека нет (хотя с помощью glibc ее можно создать и поместить в текстовую часть).
При молчаливом раскручивании стека человеко-читаемая часть получается невнятной. Ну вот простой пример из реальности: открываем файл A.xml, и получаем сообщение, что не найден файл B.xsl. С каких, спрашивается хренов? При правильном подходе слой, ответственный за разбор файла, должен поймать это исключение, добавить к нему спереди сообщение "Файл A.xml содержит ссылку на файл DTD B.xsl," и отправить дальше.
Но, важный момент, отправить дальше то же самое исключение, а не создать новое, чтобы не потерять трассировку стека. Или как вариант создать новое, но положить в него трассировку стека от оригинала.
На самом деле понятно, что все как в оригинальном посте по ссылке, не хотят с такими деталями возиться, и оставляют процесс на самотек, и он течет как говно.
Я тут, кстати, помаленьку экспериментирую на предмет как сделать такие вещи лучше. Вот например про добавление информации у меня есть идея примерно такая:
try ("File '%s' contains a link to DTD file '%s', failed to parse the DTD:", xmlfile, dtdfile) {
opendtd(dtdfile); ...
};
Чтобы значить эти сообщение при раскрутке стека автоматически собирались.
no subject
Date: 2012-08-30 04:10 pm (UTC)sub ASSERT() { my( $self, $condition, $error_msg ) = @_ ; unless( $condition ) { my ( $package , $filename , $line , $subroutine ... ) = caller(1) ; # NOTE: in graphical mode .... my $out = $self->get_out() ; my $disp_msg = "INTERNAL ERROR\n" . "> " . $error_msg . "<\n" . "\t\t in " . $line . " " . $package . "::" . $subroutine . "\n\n" ; # print $out $disp_msg ; die $disp_msg ; } # assert fails } # ASSERT $console->ASSERT( open (my $log_fh, ">$log_fl_name") , "Cannot open a file \"" . $log_fl_name . "\"\n\t" . $! ) ; $console->ASSERT( -w $out_path , "Output path " . $out_path . " is not writeable." ) ; $console->ASSERT( not(defined(some_hash->{$mod_name})) , $some_name . " defined twice. Please remove the older version." ) ; $console->ASSERT( $is_type_found , $header_str . "\n first string does not contain type." ) ;no subject
Date: 2012-08-30 04:20 pm (UTC)no subject
Date: 2012-08-30 04:44 pm (UTC)Ошибки нижнего уровня сидят в переменной $!
Но в большинстве случаев они тоже не нужны.
Вложенные ошибки не нужны, так как оно падает прямо там, где споткнулось.
no subject
Date: 2012-08-30 05:50 pm (UTC)Вообще говоря, из приведенного кода оно никак не следует. Видно, что caller() возвращает только последнюю точку вызова, а никак не стек. Или если оно что-то большее возвращает, то это большее выбрасывается.
Стек в большинстве случаев нужен для отслеживания ошибок в программе.
> Ошибки нижнего уровня сидят в переменной $!
Не про них речь. А про что-нибудь вроде
sub XXX {
...
$console->ASSERT( open (my $log_fh, ">$log_fl_name") , "Cannot open a file \"" . $log_fl_name . "\"\n\t" . $! ) ;
...
}
$console->ASSERT( &XXX() , "XXX failed") ;
Если файл не откроется, никто не будет знать, накой функции XXX понадобился этот файл.
> Вложенные ошибки не нужны, так как оно падает прямо там, где споткнулось.
В этом и недостаток по сравнению с исключениями. Исключения предполагают, что необязательно падать там, где споткнулось, а можно перехватить.
no subject
Date: 2012-08-30 08:13 pm (UTC)Если не знать, что это могут быть за ошибки.
Assert - это в чистом виде предположение, инвариант правильной работы.
Тут информация должна быть компактной и точной. В частности, данный вариант сообщает, в какой функции ошибка, вынимая минимальную и достаточную информацию из стека.
Если файл не откроется, никто не будет знать, накой функции XXX понадобился этот файл.
Пардон. Если не открывается лог-файл, то это ситуация, когда программа делает харакири и администратор с программистом идут разбираться.
Стандартная ситуация, когда файл может не открыться - это просто проверка кода возврата и два варианта штатного выполнения.
Класс не найден - это исключение. Читали, нашли конец файла и начали бросаться исключениями - это порнография.
Исключения предполагают, что необязательно падать там, где споткнулось, а можно перехватить.
Исключение - это исключение. Правильный вариант перехвата - отстрелить заражённый кусок и исполнять то, что явно ошибкой не затронуто. Самое правильное, если это локализовано сразу в отдельный изолированный процесс.
sub some_func() { ... my $some_structure = undef ; eval { # тут дофига всего делается и с внешних ресурсов структура заполняется }; # eval if( $@ ) { $console->WARN( "ERRORS by reading некая структура в файле и куча чего ещё.\n" . $@ # Тут будет сообщение от ASSERT со стеком и прочей нужной информацией . "\n The некая структура " . $some_file_path ." is not processed" ); # REM die ... <- стандартно оно дохнет, но у нас есть стоящее ниже # REQ пустая структура - это уже проблемы пользователя. Отдаём как undef - пусть разбирается. } # if error return $some_structure ; } # some_func()И, если получатель результата начинает лезть в структуру, не проверив её на нуль, то это надо лечить канделябром.
Короче говоря, изначально в готовой программе все предположения валидируются через assert. Если при разборе ошибок оказывается, что это не исключение, а штатная ситуация, то код дополняется обработчиком этой ситуации и тестируется на эту обработку.
Ладно, похоже где-то в базовых вещах вышли траблы с коммуникацией, так что я сейчас напишу разъясняющий пост, а на этот надо отвечать, после того, как тот будет прочитан.
no subject
Date: 2012-08-31 06:05 pm (UTC)Этой информации достаточно только для очень примитивных программ. В программах сколько-нибудь заметного размера ее недостаточно. Ну вот в качестве простого примера, скажем есть у вас функция, которая получает имя файла как параметр, пытается открыть файл, и умирает при неудаче с сообщением "Can not open file '%$^&&*': no such file." Очевидно, что она получила кривое имя файла, но откуда и почему она его получила - без полного стека не разобраться.
Но, опять же, стек - это для программиста с исходным кодом и возможно отладчиком. Пользователю он не поможет. Для пользователя по-хорошему сообщения тоже должны раскрутиться как стек, и каждый фнукциональный уровень должен рассказать, что и почему он пытался делать, и в итоге каким образом ошибка произошла из действий пользователя.
>Класс не найден - это исключение. Читали, нашли конец файла и начали бросаться исключениями - это порнография.
Тут мы вроде согласились :-)
Вообще мой изначальный пост пытался выразить мысль, что бесконтрольно бросаться исключениями и надеяться, что они где-то правильно обработаются - безобразие.
Наука против магии
Date: 2012-08-31 07:15 pm (UTC)В наших подходах очень серьёзное отличие как раз в том, что я знаю, что нужно для правильного выполнения и проверяю инварианты.
То есть, вокруг всего стоят заборы, куда ошибка пройти не может.
В частности,
Для пользователя по-хорошему сообщения тоже должны раскрутиться как стек, и каждый фнукциональный уровень должен рассказать, что и почему он пытался делать, и в итоге каким образом ошибка произошла из действий пользователя.
Если пользователь задаёт файл на чтение - тут же идёт проверка того, что файл есть и чтение разрешено. Если нет, то не там где-то в глубине вывалится ошибка о невозможности открытия, а сразу уже в интерфейсе будет сказано, что файл не существует или нет прав доступа.
Абсолютно аналогично защищены функции внутри программы. Паранойя - наше всё.
Далее, если ошибка произошла, то это идёт в руки программисту и он первым делом делает предположения о возможных причинах. То есть он не гадает по стеку, а выдвигает гипотезу (о своей ошибке или не правильном предположении), после чего меняет программу, стоит новые заборы и пытается гипотезу проверить.
Так как заборов много и ошибка не успевает далеко убежать от источника, всё выясняется очень быстро, в большинстве случаев без дебаггеров и хитрых отладок. По крайней мере, если сравнивать то, как работаю я и как коллеги. Хотя, существуют кудесники дебаггера, прогоняющие в нём всю программу и следящие за состоянием двух десятков переменных. Но я для таких фокусов не настолько мудр.
Последнее. Если программист где-то генерирует или получает имя файла, то assert будет стоять именно в том месте на том же уровне (может быть отлавливая ошибку в нижних и передавая её наверх). То есть, ошибка в лог уйдёт именно с того места, где известно, что это за файл и зачем он нужен.
no subject
Date: 2012-08-31 08:23 pm (UTC)no subject
Date: 2012-08-31 08:41 pm (UTC)2. При правильном процессе разработки ошибки находятся в тестировании, а не в поле. Наша задача поймать ошибки на начальных этапах. Тут жирные логи только мешают.
3. Если программист не имеет понятия, почему его программа может упасть, гнать ссанными тапками из профессии.
И в который уже раз: вопрос параметров. При желании можно сделать, чтобы программа валила в дамп всё состояние и все исходные данные, включая размер бюста пользователя.
Разъяснения
Date: 2012-08-30 08:45 pm (UTC)1. ASSERT - это не инструкция препроцессора и не функция, а метод класса с интерфейсом DiagnoseConsole
В данном случае он получает два параметра:
а) предположение программиста, на основании которого он пишет код
б) сообщение, необходимое для того, чтобы понять, почему это предположение не выполняется.
В принципе, туда можно напихать и других параметров, включая стек вызова, но практика показывает, что это не нужно. По крайней мере, для моих задач.
2. Интерфейс DiagnoseConsole может быть реализован любым удобным образом. В приведённом примере это вариант для консольного приложения. Для оконного или сервера действия могут быть совершенно другими.
Четыре примера использования, стоящие внизу, при этом не меняются. Просто в класс при создании передаётся другой объект $console. Или же объект подменяется в процессе выполнения. Скажем, консольное приложение открыло GUI и перенаправило диагностику в одно из окон.
Обычно я использую глобальный объект, просто чтоб не возиться. Но были проекты, где все (бизнес) классы реализовывали интерфейс DiagnoseConsoleUser
3. Объект $console может быть параметрируемым. То есть поведение метода ASSERT может меняться динамически. Это может быть также просто оболочка, куда реализация подсовывается динамически. Всё зависит от задач.
4. Строчка
die $disp_msg ;
- это вариант идеологически правильного поведения, позволяющего с наименьшей кровью получить правильно работающую программу.
Параметрируемость консоли, расширяемость иерархии классов и возвращаемые значения функции caller() позволяют заниматься извращениями любого вида. Собственно, для этого и стоило городить огород.
Если ко мне приходит проджект менеджер со словами "Завтра сдавать клиенту, а нифига не работает! Нужно всё спрятать!", я молча ставлю впереди этой строчки решетку. В результате программа проскакивает мимо комментария и портится где-то внутри, не показывая ничего пользователю.
Если приходит Вася и говорит, что ASSERT валится в таком-то месте по неизвестной причине и разбираться с этим времени нет, я вставляю проверку на имя вызывающей функции и в этом особом случае вместо die и откидывания копыт, скажем, посылаю этому самому Васе мейл вместе с двумя мегабайтами хеша.
Идея в том, чтобы огородить "правильный путь", и получить ошибку сразу, если предположение, использованное при разработке программы оказалось не верным. Вот когда ошибка в рассуждениях и предположениях программиста выявлена (причём не закопана где-то в мегабайтах логов, а зримо выразилась в упавшей программе, можно принимать решение о том, исправлять ли ситуацию, игнорировать, оставить на потом или просто закрыть глаза.
Последнее: хотелось бы в журнале видеть стиль более вменяемый, чем стандартный.
no subject
Date: 2012-08-31 06:09 pm (UTC)4. confess() - это более правильный вариант die(). Который делает все то же самое, плюс выводит полный стек.
Стиль: самый вменяемый и есть. Причем вроде как нынче доступный только для исторических журналов, а в новых они дают только ужасные стили.
no subject
Date: 2012-08-31 07:23 pm (UTC)Жадность - плохое чувство, графомания - плохая привычка. Если информации слишком много, полезная содержательная часть утопает в мусоре.
По сути, в примере assert собирает всю нужную информацию, сохраняет её в предназначенном месте, после чего убивает испорченную часть. (Или не убивает, если у нас задача замести мусор под ковёр) Стек используется, но не весь, а только та часть, которая нужна. И очень-очень редко это полный стек до самых истоков.
Первым делом, это сохраняет время на поиск ошибок и их осознание. После чего гораздо дешевле по времени и сложности поковырять код и проверить причины ошибки, а не колдовать над стеком.
Ну и, да, ошибку можно перехватить в дебаггере и подняться наверх, что особенно полезно, если она пришла из компонентов, созданных любимыми коллегами.
Ещё одно добавление
Date: 2012-08-30 09:13 pm (UTC)no subject
Date: 2012-08-31 06:10 pm (UTC)no subject
Date: 2012-08-29 09:02 am (UTC)Жесть какая. Вы представляете себе, что будет твориться с программой, которая при невозможности выделить ресурс будет тихо игнорировать этот факт продолжать работать дальше как-будто ничего не случилось?
>или же компилятор должен проверять,
Ну да, ну да. При наличии полиморфизма времени выполнения пытаться решить проблему во время компиляции. Да ради бога, собственно. Осталось совсем чуть-чуть: все без исключения методы должны иметь спецификацию исключений. О размере кода и количестве проблем при поддержке можно будет только гадать.
no subject
Date: 2012-08-29 02:36 pm (UTC)Ничего более худшего, чем от непойманного исключения. Как максимум - кору вывалит, которую потом можно будет изучить и найти, что и где не так. В то время, как от непойманного исключения программа молча загнется без коры.
>При наличии полиморфизма времени выполнения пытаться решить проблему во время компиляции.
От полиморфизма времени исполнения один вред.
>Осталось совсем чуть-чуть: все без исключения методы должны иметь спецификацию исключений.
В джаве - именно так. Поэтому в ней исключениями почти что можно пользоваться.
no subject
Date: 2012-08-29 02:45 pm (UTC)Вы всерьез не вдупляете, что упасть программа может совсем не там, где исключение было тихо проигнорировано?
>В то время, как от непойманного исключения программа молча загнется без коры.
Это откуда такая глупость?
>От полиморфизма времени исполнения один вред.
Предложите решение лучше полиморфизма времени исполнения.
>В джаве - именно так. Поэтому в ней исключениями почти что можно пользоваться.
Как раз в ней исключениями пользоваться практически невозможно. google -> "exception specification evil"
no subject
Date: 2012-08-29 03:03 pm (UTC)Может, конечно. Но обычно сразу.
>>В то время, как от непойманного исключения программа молча загнется без коры.
>Это откуда такая глупость?
Из практики.
>Предложите решение лучше полиморфизма времени исполнения.
Очевидно, что полиморфизм времени компиляции.
>Как раз в ней исключениями пользоваться практически невозможно. google -> "exception specification evil"
Зачем мне читать людей, у которых мозга нет?
no subject
Date: 2012-08-30 07:18 pm (UTC)no subject
Date: 2012-08-30 03:35 pm (UTC)заставить компилятор проверять код на отсутствие исключений - интереснее, но в реальной жизни даже банальное создание std::string это неизбежно один-два new
а каждый new может кинуть, поэтому в такой блок что ты хочешь засунуть можно будет только что-то весьма ограниченное
no subject
Date: 2012-08-30 04:14 pm (UTC)Вообще обрабатывать окончание памяти как исключение обычно бессмысленно. Единственное, что можно сделать - это запустить хитрый обработчик, который попытается поудалять всякие не очень нужные вещи, и потом продолжить как обычно. А так если память кончилась, то кончилась, все, пипец. Ну, теоретически можно порассуждать про "ах, память кончилась в результате попытки выполнения запроса, и вот сейчас мы этот запрос прервем, и все вернется в норму", но на практике так не бывает.
no subject
Date: 2012-08-30 05:00 pm (UTC)я согласен что перехватывать его имеет смысл только в ограниченном числе случаев, но это просто пример того почему компилятор тебе не даст сделать код не кидающий экзепшны
кстати, я вот позабыл, nothrow отвечает только за код конкретной функции? вложенные функции его не интересут?
no subject
Date: 2012-08-30 05:54 pm (UTC)>nothrow отвечает только за код конкретной функции? вложенные функции его не интересут?
Это который пустой throw()? Вроде должен обещать, что и вложенные функции тоже не кидают.
no subject
Date: 2012-08-30 06:28 pm (UTC)class bad_alloc : public exception
т.е. от std::exception унаследован
просчитать все варианты компилятор не может, особенно учитывая всевозможные перегрузки с помощью виртуальных функций. поэтому может компилятор что-то и проверит, но полагаться на этой всегда и везде не получится
no subject
Date: 2012-08-30 07:13 pm (UTC)С виртуальными функциями, кстати, вопрос уже решен: какой throw декларирован в родительском классе, такой и наследуется детьми. Они вроде его могут уменьшить, но не могут увеличивать.