Комментарии 24
А на практике мы имеем привычку приложений, написанных на языках без исключений, падать без намека на причину.
Пишу на Go и должен подтвердить, волей не волей начинаешь обрабатывать все ошибки, которые стоит обрабатывать. Хоть мои приложения и небольшие, но они ни разу ещё не падали. Обнаруживают ошибку — пишут в лог, прерывают выполнение текущего действия и продолжают работать как ни в чём не бывало.
А если добавить ещё статическую типизацию, то по сравнению с Python код начинает работать правильно с первого раза гораздо чаще. Если на Python я просто жду пока проблемный участок не выполнится и не скажет мне где я попутал имена переменных или ещё чего, то Go говорит мне об этом на стадии компиляции, и это здорово.
Мне всё же кажется, что исключения более разумная политика сообщений об ошибках в большинстве случаев. Прежде всего они не допускают непредсказуемого поведения программы. Если кто-то не прочитал доки к функции (или прочитал, но забил) с исключениями, то ошибка всё равно вылезет, причем именно в месте её возникновения, и не позволит дальнейшее выполнение программы. А в случае возврата кодов ошибка может вылезти в «километрах» от места её возникновения и, главное, её проявление может быть самым разным, вплоть до катастрофического.
Например, есть функция, формирующая where часть SQL-запроса. Имеется в виду что функция возвращает строку типа " WHERE id = ". Если сделать на ошибочное n возврат пустой строки (или приводимое к ней значение типа null или false), как флага ошибки, и забыть его проверить, то можем получить где-то запрос «DELETE FROM table», который успешно выполнится и спокойно удалит все строки из таблицы вместо одной. В случае с необработанным исключением мы бы получили краш. Спросите пользователей, что им лучше: краш при попытке удаления одной записи или тихое удаление всех?
Например, есть функция, формирующая where часть SQL-запроса. Имеется в виду что функция возвращает строку типа " WHERE id = ". Если сделать на ошибочное n возврат пустой строки (или приводимое к ней значение типа null или false), как флага ошибки, и забыть его проверить, то можем получить где-то запрос «DELETE FROM table», который успешно выполнится и спокойно удалит все строки из таблицы вместо одной. В случае с необработанным исключением мы бы получили краш. Спросите пользователей, что им лучше: краш при попытке удаления одной записи или тихое удаление всех?
В Go если функция может вернуть ошибку, то она передается отдельным возвращаемым значением, например:
Это предотвратит и крэш и удаление всех записей.
n := -1
if whereString, err := whereFunc(n), err != nil {
fmt.Fprint(os.Stderr, err)
return;
}
deleteFromTable(whereString)
Это предотвратит и крэш и удаление всех записей.
Но если написать
то результат будет е очень хорший. А уж если допустим синтаксис
то совсем плохой
n := -1
whereString, err := whereFunc(n)
deleteFromTable(whereString)
то результат будет е очень хорший. А уж если допустим синтаксис
<source lang="go">
n := -1
whereString := whereFunc(n)
deleteFromTable(whereString)
то совсем плохой
Компилятор не позволит написать второй вариант. Должно быть как минимум
Так вы явно указываете, что вам пофиг на возвращаемую ошибку. И позже такие места легко найти в коде и дополнить их обработчиками ошибок.
А если err указан, но не используется, то компилятор и на это сругается.
n := -1
whereString, _ := whereFunc(n)
deleteFromTable(whereString)
Так вы явно указываете, что вам пофиг на возвращаемую ошибку. И позже такие места легко найти в коде и дополнить их обработчиками ошибок.
А если err указан, но не используется, то компилятор и на это сругается.
Я думаю VolCh имел в виду не я зык Go, а то, что существуют другие языки без механизма исключений, в которых такой синтаксис будет корректным и приведет к драматичному исходу.
Ну, с таким контролем со стороны компилятора часть проблем (не знал или забыл) снимается. Но только часть — «не должно быть ошибки» или «потом обработчик сделаю» остается. Но когда ошибка всё-таки возникла, то локализовать её может быть весьма затруднительно, а проявления у неё самые неожиданные могут быть. С исключениями проще — место четко локализовано, а проявление — краш.
НЛО прилетело и опубликовало эту надпись здесь
Гугл в Go, убрав исключения, предусмотрел соответствующие механизмы. Например, возможность одним return'ом возвращать несколько значений. Это позволяет не ломать основную логику работы.
Значение возвращаемое функцией должно принадлежать одному пространству значений. Для реализации этого в традиционных языках требуются костыли, привносящие дополнительные издержки…
Значение возвращаемое функцией должно принадлежать одному пространству значений. Для реализации этого в традиционных языках требуются костыли, привносящие дополнительные издержки…
НЛО прилетело и опубликовало эту надпись здесь
Добавление переменной по ссылке в аргументы функции — довольно старый способ и выглядит не хуже, чем try-catch.
Не хуже. пока вам нужно передавать данные об ошибке ровно на один уровень вверх… Когда же вдруг выяснится, что обрабатывать ошибку надо 4-5 уровней выше по стеку вызовов, вам тут же станет грустно…
А если же друг окажется, что на уровнях выше надо и другие ошибки передавать выше, вы добавите еще одну переменную в аргументы?
Нельзя в слое контроллера обрабатывать ошибки модели — что мы сможем сказать на этом уровне пользователю? Почему вообще должен слой контроллера знать хоть что-то об имлементации модели?
То, что вы можете обработать на месте возникновения ошибки, должно обрабатываться на месте. Если нет, то оно должно обрабатываться там, где это проще всего обработать. Если Exception модели не перехватили там где следовало это сделать, и выпустили в контроллер, то это не проблема исключений, это проблема кривых рук программиста…
Поверьте, нет такого метода обработки ошибок, который бы избавил программиста от необходимости иметь мозги. В конце-концов, вы можете не пользоваться исключениями для обработки ошибок. Вам нравится после каждого вызова функции писать кучу if/сase для анализа всех возможных в этом месте ошибок, то бог вам в помощь…
Я в своё время с трудом въезжал в исключения. Но теперь, пожалуй вряд ли смогу от них отказаться… Отсутствие исключений в Go это почти единственная причина, по которой я не решаюсь применять его в реальных проектах, как один из языков разработки…
Например, если в контроллере есть catch MemcachedConnectionTimeoutException — значит контроллер знает про Memcached, а он вообще о модели знать ничего не должен, кроме API модели.
Оставим за рамки входит ли Memcached в модель, но, да, соглашусь, что если входит, то ловить MemcachedConnectionTimeoutException в контроллере странновато. Это исключение должно перехватиться в модели и должно либо быть корректно обработано (повторный запрос, работа без Memcached и т. д.), либо сгенерировать исключение типа Model*Exception, которое будет входить в контракт модели и ловить которое контроллеру позволительно без нарушения инкапсуляции модели. Если же в ней оно не ловится, то должно идти именно на самый верхний уровень — сообщить пользователю «упс, что-то пошло не так, мы работаем над этим» и записать лог и/или отправить сообщение разрабам/админам.
Исключения позволяют писать на порядки более простой (а значит и надежный) код за счет отделения прикладного кода от обработки ошибок.
Это окупает любые недостатки присущие исключениям.
(Под более простым кодом я понимаю например линейный код, вместо кода с ветвлениями, каждую ветку которых надо отлаживать или покрывать юниттестами, что возможно только для простейших случаев)
Хотя приведенный здесь аргумент, что любая функция может вызвать исключение, и можно забыть его обработать, — не годится.
1) У кодов возврата такая же проблема. Каждая функция может возвращать код возврата и его можно забыть обработать.
2) Суть исключений в том что их надо не ловить на каждый чих, а обработать для какого-то крупного логически завершенного блока из нескольких вычислений. В правильно построенной программе вообще очень мало мест где надо ловить исключения.
Это окупает любые недостатки присущие исключениям.
(Под более простым кодом я понимаю например линейный код, вместо кода с ветвлениями, каждую ветку которых надо отлаживать или покрывать юниттестами, что возможно только для простейших случаев)
Хотя приведенный здесь аргумент, что любая функция может вызвать исключение, и можно забыть его обработать, — не годится.
1) У кодов возврата такая же проблема. Каждая функция может возвращать код возврата и его можно забыть обработать.
2) Суть исключений в том что их надо не ловить на каждый чих, а обработать для какого-то крупного логически завершенного блока из нескольких вычислений. В правильно построенной программе вообще очень мало мест где надо ловить исключения.
Присоединяюсь — лучше написать я-бы не смог.
Я довольно много писал на С, и добрая половина строк кода там — обработка веток всевозможных ошибок — написание которых, это выброшенное на ветер время (а делать приходится).
А практически каждый мой код-ревью Сишного кода заключался в добавлении кучи проверок, которые многие (особенно начинающие) программеры забывают добавлять, тем самым приводя программу к непредсказуемому поведению в непредсказуемых местах (далеко от места ошибки).
Отлаживать и исправлять необработанные исключительные ситуации на порядок проще, чем необработанные коды ошибок.
Я довольно много писал на С, и добрая половина строк кода там — обработка веток всевозможных ошибок — написание которых, это выброшенное на ветер время (а делать приходится).
А практически каждый мой код-ревью Сишного кода заключался в добавлении кучи проверок, которые многие (особенно начинающие) программеры забывают добавлять, тем самым приводя программу к непредсказуемому поведению в непредсказуемых местах (далеко от места ошибки).
Отлаживать и исправлять необработанные исключительные ситуации на порядок проще, чем необработанные коды ошибок.
Однако все эти удобства имею стоимость, которую легко сформулировать:Исключения учат заботиться об ошибках не везде и постоянно, а только там и тогда, когда их реально как-то обработать.
Исключения учат разработчиков не заботиться об ошибках.
Есть такого рода ошибки, которые невозможно правильно обработать на уровне абстракции приложения где это случилось… Например: Вы соединяетесь с серверной частью и запрашиваете данные через низкоуровневый сокет, но внезапно серверная часть упала… по логике приложения вам надо cостыковаться с другим сервисом используя совершенно другой API. Для этого надо переинициализировать объект отвечающий за выдачу этих данных…
На уровне сокета вы можете получить ошибку, но что-то с ней сделать вменяемое не получится. Её придется выводить наверх…
При традиционном методе, вам придется сохранять значение в глобальной переменной и вписывать в середину кода метода/процедуры return'ы. И вернувшись на нужный уровень отследить значение этой глобальной переменной и обработать ошибку на этом уровне… Будет ли такой код более читабелен, чем с исключениями — спорный вопрос.
Вообще говоря это всё те же исключения только выполняются они руками… И такая реализация потребует кучу дополнительных return'ов и возможно преобразований типов и прочая… Ну и смысл?
Мы сначала пишем базовый функционал, демонстрируем клиенту, а потом доводим до ума обработку нештатных ситуаций… В противном случае сроки будут слишком велики…
На уровне сокета вы можете получить ошибку, но что-то с ней сделать вменяемое не получится. Её придется выводить наверх…
При традиционном методе, вам придется сохранять значение в глобальной переменной и вписывать в середину кода метода/процедуры return'ы. И вернувшись на нужный уровень отследить значение этой глобальной переменной и обработать ошибку на этом уровне… Будет ли такой код более читабелен, чем с исключениями — спорный вопрос.
Вообще говоря это всё те же исключения только выполняются они руками… И такая реализация потребует кучу дополнительных return'ов и возможно преобразований типов и прочая… Ну и смысл?
Мы сначала пишем базовый функционал, демонстрируем клиенту, а потом доводим до ума обработку нештатных ситуаций… В противном случае сроки будут слишком велики…
Одна проблема в том, что используя стороннюю библиотеку или просто чужой код, нельзя сказать упадет он или нет и если упадет, то когда, поэтому приходится либо ловить все подряд, либо изучать исходники. Вторая проблема — это тьма объявленных исключений в стороннем API помноженная на свои собственные. И третья проблема — авторы редко задумываются об осмысленном тексте для исключений, что приводит потраченному времени, особенно на простейших ошибках.
Например, код, который обеспечивает повтор RMI вызова, практически невозможно написать без ошибок (тут конечно и заслуга самого RMI).
Например, код, который обеспечивает повтор RMI вызова, практически невозможно написать без ошибок (тут конечно и заслуга самого RMI).
Почему то мне кажется странным наличие такого холивара, то есть странно, что кто то хочет повсюду коды ошибок без разбора использовать, или наоборот исключения. Как мне кажется сценарии достаточно хорошо определяют что нужно в данный момент, код или исключение.
Как пример, если мы делаем поиск по словарю и у нас нет значения, то если у нас заранее определенное количество значений, то неизвестный ключ должен вызывать исключение. А если заранее неизвестно, например обработка данных полученных из сети, или разбор XML и нужно известные обработать, неизвестные проигнорировать, то код ошибки.
Если у нас какое то длительное вычисление и любая ошибка делает результат невалидным — исключение.
Если же это поток мелких действий, и это ожидаемо, что принятое для обработки сообщение может оказаться «плохим», то код ошибки.
Исключение удобно, когда много кода, ошибка может возникнуть в любом месте и ее придется пробрасывать наверх через десяток методов, и нужно много кодов ошибки. Исключение позволяет спрятать передачу «кода ошибки», так же как вызов метода у объекта прячет this.
Код ошибки хорош тем, что ошибка может случиться сотню и тысячу раз в секунду, и те десятки тысяч правильных сообщений не пострадают, Исключение же, случившееся пару сотен раз в секунду займет 100% процессора и не даст выполнять полезную работу.
Как пример, если мы делаем поиск по словарю и у нас нет значения, то если у нас заранее определенное количество значений, то неизвестный ключ должен вызывать исключение. А если заранее неизвестно, например обработка данных полученных из сети, или разбор XML и нужно известные обработать, неизвестные проигнорировать, то код ошибки.
Если у нас какое то длительное вычисление и любая ошибка делает результат невалидным — исключение.
Если же это поток мелких действий, и это ожидаемо, что принятое для обработки сообщение может оказаться «плохим», то код ошибки.
Исключение удобно, когда много кода, ошибка может возникнуть в любом месте и ее придется пробрасывать наверх через десяток методов, и нужно много кодов ошибки. Исключение позволяет спрятать передачу «кода ошибки», так же как вызов метода у объекта прячет this.
Код ошибки хорош тем, что ошибка может случиться сотню и тысячу раз в секунду, и те десятки тысяч правильных сообщений не пострадают, Исключение же, случившееся пару сотен раз в секунду займет 100% процессора и не даст выполнять полезную работу.
Собственно слово «исключение» об этом и говорит. Исключение должно бросаться в исключительных ситуациях, когда продолжение работы текущей функции/метода/слоя невозможны. Функция ищет что-то в базе, но база отвечает или ошибка синтаксиса sql — исключение. Но никто (вроде бы) не предлагает бросать исключения в функции поиска подстроки в строке, если она не найдена.
Пожалуй, можно сформулировать правило выбора так: ситуация описана в бизнес-логике (логике предметной области) — ошибка, не описана (какая-то техническая проблема или на вход приходит то, что не должно прийти) — исключение. При этом стоит иметь в виду, что в большинстве случаев пользовательский ввод и вообще внешние данные (файлы, БД, сеть и т. п.) должны вызывать ошибку, а вот невозможность их получить — исключение. Разве что подразумевается, что внешние данные пишутся самой программой, конкретно для неё или соответствуют какому-то стандарту — тогда исключение (хотя со стандартами всё сложно).
А вот сотни и тысячи раз за секунду — это уже из области оптимизации, а не «идеального» кода. :)
Пожалуй, можно сформулировать правило выбора так: ситуация описана в бизнес-логике (логике предметной области) — ошибка, не описана (какая-то техническая проблема или на вход приходит то, что не должно прийти) — исключение. При этом стоит иметь в виду, что в большинстве случаев пользовательский ввод и вообще внешние данные (файлы, БД, сеть и т. п.) должны вызывать ошибку, а вот невозможность их получить — исключение. Разве что подразумевается, что внешние данные пишутся самой программой, конкретно для неё или соответствуют какому-то стандарту — тогда исключение (хотя со стандартами всё сложно).
А вот сотни и тысячи раз за секунду — это уже из области оптимизации, а не «идеального» кода. :)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Крэши, вызванные исключениями