что такое код возврата
Коды возврата & исключения
Замечательные статьи публиковались в последнее время, хотелось бы добавить ещё несколько абзацев по данной теме.
Уважаемые авторы предыдущих топиков как-то упускали тот момент (или мне показалось? или это само-собой подразумевается?) что exceptions возникли как инструмент для решения весьма утилитарной задачи — передачи управления из места возникновения ошибки в то место, где она может быть обработана.
Немного истории, чтобы понятно было, откуда такая задача возникла. В каждом более-менее нетривиальном программном обеспечении (сложнее, чем «Hello, world», да) всегда существуют точки, где нормальное выполнение не может продолжаться — I/O подсистема выдала отказ, памяти для алгоритма почему-то не хватило, входные параметры для функции ей не понравились и т.п. Как именно реагировать?
Ситуация становится ещё печальней, если рассматривать создание какой-либо библиотеки, которая должна будет использоваться в других проектах. Мы не можем (как следствие) вызывать assert/abort или ещё какую-нибудь подобный обработчик — откуда мы знаем, что имеем право завершать работу всего приложения? К примеру, наша библиотека занимается сбором какой-то статистики входных данных, а из-за такого её поведения будет остановлена работа всего устройства. А пишем мы firmware для кардиостимулятора, конечно же.
Ок, abort() — не годится. Мы не хотим создавать приложения, подобные воздушному шарику — в любом месте уколол, весь шарик умер. Мы хотим пользоваться технологией, которая позволит разделить место, где возникла ошибка, и место где принимается решение о том, что именно мы будем делать с этой ошибкой. Так как для одних применений реакцией будет запрет сбора этой статистики, для других — переинициализация библиотеки (например, с другими параметрами), где-то — просто игнорирование. Так как на более высоком уровне доступно гораздо больше информации о том, как реагировать на возникшую ошибку.
Собственно, написано это всё было для того, чтобы лучшее понимание «для чего именно» служат exceptions позволяло лучше понимать «а как их применять и для каких случаях».
PS: ещё было желание показать на примерах кода, какие именно минусы есть у exceptions. И как решалась задача «и ошибки удобно обрабатывать и exceptions при этом не использовать» (актуально для встроенных систем, когда «кардиостимуляторы» пишутся). Но это позже.
Коды возврата vs исключения: взгляд с колокольни
Просмотрев пост Коды возврата vs исключения и комментарии к нему, я заметил, что в обсуждении упущена одна нить, краткий тезис которой следующий: в некоторых языках такая проблема даже не стоИт, т.к. вопрос «что выбрать, коды возврата или исключения» в таком языке является низкоуровневым. Как, например, не стоит вопрос, каким образом реализовать конструкцию «foreach». Т.к. для программиста, использующего тот же «foreach», нет никакой разницы, использовали ли создатели языка while или for или что-то еще в имплементации данного оператора. Главное это паттерн, который представляет собой этот самый оператор.
Посмотрим, они работают.
process-retcode-answer
( defn ok-processor [ result ]
( println ( str «ok. result: » result ) ) )
( defn error-processor [ result ]
( println ( str «error. result: » result ) ) )
( defn another-error-processor [ result ]
( println ( str «another error. result: » result ) ) )
( defn unknown-error-processor [ result ]
( println ( str «unknown error. result: » result ) ) )
Определяем map кодов возврата на наименования обрабатывающих их функций:
( def result-mapping < 0 'ok-processor
— 1 ‘error-processor
— 2 ‘another-error-processor
: other ‘unknown-error-processor > )
Теперь создаем тестовые подпрограммы, возвращающие разные коды возврата и соответствующий результат:
Работа нашего оператора в данном случае выглядит так:
Здесь в каждое тело состоит только из одного метода. В реальности можно вместо него вставить любую последовательность функций.
Преимущество данного оператора состоит в том, что обработка новых кодов или изменение обработчиков существующих, осуществляется прозрачно путем реализации обработчиков и внесением соответсвующих изменений в mapping, который может находиться в отдельном файле.
Никаких if-ов. Данный подход реализует достаточно гибкий паттерн обработки кодов возврата.
process-exception
По аналогии с предыдущим примером. Есть функции обработки некоторых исключений:
( defn arithmetic-exception-processor [ e ]
( println ( str «Arithmetic exception.» ) ) )
( defn nullpointer-exception-processor [ e ]
( println ( str «Nullpointer exception.» ) ) )
( defn another-exception-processor [ e ]
( println ( str «Other exception.» ) ) )
Map исключений на наименования обрабатывающих их функций:
Создаем тестовые подпрограммы, генерирующие разные исключения:
Работа нашего оператора:
( process-exception exception-mapping
( test-call-ok ) )
«test result»
Замечания в конце описания предыдущего оператора применимы и здесь.
Выводы
Данные операторы очень похожи и в принципе реализуют один и тот же паттерн обработки ответа, но с разной реализацией. Здесь я предпочтение отдаю process-retcode-answer. Хотя в других языках варианты с кодами возврата не всегда выгодны в сравнении с вариантами, использующих исключения (конечно, зависит от условий задачи и самого языка — это уже обсудили).
Вот вариант реализации вышеописанных операторов:
( defmacro process-exception [ mapping & body ]
( let [ catch-items ( map ( fn [ m ]
` ( catch
( eval ( second m ) ) e# ) ) )
( eval mapping ) ) ]
` ( try
( defmacro process-retcode-answer [ mapping & body ]
` ( let [ answer# ( do
@body )
retcode# ( first answer# )
result# ( second answer# )
processor# ( get
mapping ) processor# ) ]
( ( eval processor# ) result# ) ) )
Этот довольно утрированный пример показывает, что базовые элементы языка не сильно важны, как возможность расширять язык новыми конструкциями и также иметь в нем те же функции высокого порядка, лямбды, замыкания. Такой язык позволяет программисту стать художником. Он не пишет паттерны снова и снова. Он просто «создает» язык, в котором может наиболее естественно, наглядно и кратко сформулировать решение поставленной перед ним задачи. Профессия становится не ремеслом, а искусством.
Как использовать коды завершения в Bash-скриптах
Инструменты автоматизации и мониторинга удобны тем, что разработчик может взять готовые скрипты, при необходимости адаптировать и использовать в своём проекте. Можно заметить, что в некоторых скриптах используются коды завершения (exit codes), а в других нет. О коде завершения легко забыть, но это очень полезный инструмент. Особенно важно использовать его в скриптах командной строки.
Что такое коды завершения
В Linux и других Unix-подобных операционных системах программы во время завершения могут передавать значение родительскому процессу. Это значение называется кодом завершения или состоянием завершения. В POSIX по соглашению действует стандарт: программа передаёт 0 при успешном исполнении и 1 или большее число при неудачном исполнении.
Почему это важно? Если смотреть на коды завершения в контексте скриптов для командной строки, ответ очевиден. Любой полезный Bash-скрипт неизбежно будет использоваться в других скриптах или его обернут в однострочник Bash. Это особенно актуально при использовании инструментов автоматизации типа SaltStack или инструментов мониторинга типа Nagios. Эти программы исполняют скрипт и проверяют статус завершения, чтобы определить, было ли исполнение успешным.
Кроме того, даже если вы не определяете коды завершения, они всё равно есть в ваших скриптах. Но без корректного определения кодов выхода можно столкнуться с проблемами: ложными сообщениями об успешном исполнении, которые могут повлиять на работу скрипта.
Что происходит, когда коды завершения не определены
В Linux любой код, запущенный в командной строке, имеет код завершения. Если код завершения не определён, Bash-скрипты используют код выхода последней запущенной команды. Чтобы лучше понять суть, обратите внимание на пример.
Как использовать коды завершения в Bash-скриптах
Проверяем коды завершения
После рефакторинга скрипта получаем такое поведение:
Создаём собственный код завершения
Как использовать коды завершения в командной строке
Скрипт уже умеет сообщать пользователям и программам об успешном или неуспешном выполнении. Теперь его можно использовать с другими инструментами администрирования или однострочниками командной строки.
Скрипт использует коды завершения, чтобы понять, была ли команда успешно выполнена. Если коды завершения используются некорректно, пользователь скрипта может получить неожиданные результаты при неудачном выполнении команды.
Дополнительные коды завершения
Адаптированный перевод статьи Understanding Exit Codes and how to use them in bash scripts by Benjamin Cane. Мнение администрации Хекслета может не совпадать с мнением автора оригинальной публикации.
Коды возврата vs исключения — битва за контроль ошибок
Что такое ошибка?
Как видите, много всего разного и нехорошего может произойти — а ведь это далеко не полный список :). А что делать программисту? Тут, на мой взгляд, перед нами встает очень интересный и важный вопрос — как именно нашей программе реагировать на ту или иную ошибку? Пожалуй сейчас я еще раз напомню, что излагаю свое сугубо личное мнение. И скажу следующее — как именно реагировать на ошибку целиком зависит от конкретной программы. Если у нас закончилась память в драйвере — мы должны любой ценой выжить, чтобы пользователь не получил синего экрана смерти. Если же у нас закончилась память в игрушке типа веселая ферма — то имеет смысл упасть, извиниться и попросить отправить багрепорт разработчику. Системный сервис, призванный крутиться многие месяцы без перезагрузки, должен с пониманием отнестись к ошибке CreateEvent(). Та жа ошибка в прикладной программе типа Photoshop означает что скорее всего система через секунду умрет, и лучше честно упасть, нежели попытаться проглотить ошибку, дать пользователю сохранить файл и благополучно его испортить из-за последующего сбоя во время записи. Следовательно ошибки мы можем делить на ожидаемые и неожиданные. Для разных программ и разных требований одни и те же ошибки могут считаться как ожидаемыми, так и неожиданными. С ожидаемыми ошибками мы как-то работаем. Не получилось открыть файл — говорим об этом пользователю и продолжаем работу. Не удалось выделить память для загрузки туда гигабайтного файла — говорим об этом пользователю и продолжаем работу. С неожиданными ошибками мы в большинстве случаев не работаем. Закончилась память при попытке выделить двадцать байт для создания объекта — падаем. Не создался системный объект которых на всю программу три штуки — падаем. Не читается системный пайп который по спецификации должен читаться? Лучше упасть, чем оставить программу в нестабильном состоянии и потом испортить пользователю данные. Программу он если что перезапустит, а вот за испорченный файл возненавидит до конца дней своих. А для серьезных случаев есть автосейв и перезапускающий нас ежели чего watchdog.
Что было до исключений?
Через некоторое время разработчики заметили, что большинство успешных решений использует ООП и решили что неплохо бы его вынести в синтаксис языка, дабы писать больше кода по делу и меньше — повторяющегося кода для поддержки архитектуры.
Что стало после введения исключений
Давайте возьмем код выше и посмотрим, как он трансформировался после добавления ООП в синтаксис языков программирования. Конструирование и уничтожение объектов (fopen, fclose) стало конструкторами и деструкторами. Переброс неожиданной ошибки (BOOL ret в первом примере, макрос ENSURE во втором) однозначно стал исключением.
А вот с ожидаемой ошибкой случилось самое интересное — случился выбор. Можно было использовать возвращаемое значение — теперь, когда заботу о неожиданных ошибках взяли на себя исключения, возвращаемое значение снова стало в полном распоряжении программиста. А можно было использовать исключения другого типа — если функции копирования файлов самой не нужно обрабатывать ожидаемые ошибки то логично вместо if и REQUIRE просто ничего не делать — и оба типа ошибок уйдут вверх по стеку. Соответственно, у программистов снова получилось два варианта:
Почему выжили коды возврата?
Выводы
Что бы я хотел резюмировать. На мой взгляд, большинство проблем с исключениями были вызваны первыми, не очень удачными реализациями — особенно в C++. Так что выбор «использовать только коды возврата», «использовать исключения для неожиданных ошибок и коды возврата для ожидаемых» или «использовать исключения для всего» по большей части имеется только для C++. Надеюсь, мой краткий рассказ о причинах появления исключений в современных языках программирования поможет разработчикам чуть лучше ориентироваться в современных API и замечать места, где авторы использую исключения некорректно. Понимание какие из ошибок мы считаем для нашей программы ожидаемыми, какие — неожиданные и как это оптимальным образом ложится на предоставляемую языком и API модель исключений позволяет писать простой, понятный и внимательно следящий за ошибками код.
Код возврата
При выполнении компьютерных программ операционная система создаёт сущность, называемую процессом, с помощью которой она отслеживает и ведёт учёт выполнения программы. В многозадачных операционных системах, таких как Unix или Linux, новый процесс может быть порождён существующим процессом. Процесс-создатель называется «родительским», а создаваемый процесс — «дочерним». После создания дочерний процесс выполняется параллельно с родительским. Такая техника порождения дочерних процессов используется, чтобы передать часть работы дочернему процессу, в ситуациях, когда родительскому процессу не обязательно тратить на неё время. Когда дочерний процесс завершает своё выполнение, он автоматически либо явно по указанию программиста совершает системный вызов exit, передавая ему целое число. В результате вызова exit целое число передаётся родительскому процессу, который может получить его с помощью системного вызова wait.
Связанные понятия
Сопрограммы (англ. coroutines) — методика связи программных модулей друг с другом по принципу кооперативной многозадачности: модуль приостанавливается в определённой точке, сохраняя полное состояние (включая стек вызовов и счётчик команд), и передаёт управление другому. Тот, в свою очередь, выполняет задачу и передаёт управление обратно, сохраняя свои стек и счётчик.
В теории компиляторов удалением мёртвого кода (англ. dead code elimination, DCE) называется оптимизация, удаляющая мёртвый код. Мёртвым кодом (так же бесполезным кодом) называют код, исполнение которого не влияет на вывод программы, все результаты вычисления такого кода являются мёртвыми переменными, то есть переменными, значения которых в дальнейшем в программе не используются.