Я думаю, что прочитал то же интервью с Брюсом Экелом, которое вы дали - и оно всегда меня раздражало. Фактически, аргумент был сделан интервьюируемым (если это действительно тот пост, о котором вы говорите) Андерсом Хейлсбергом, гением MS, стоящим за .NET и C #.
http://www.artima.com/intv/handcuffs.html
Несмотря на то, что я из Хейлсберга и его работ, этот аргумент всегда казался мне фальшивым. Это в основном сводится к:
«Проверенные исключения являются плохими, потому что программисты просто злоупотребляют ими, всегда ловя их и игнорируя их, что приводит к скрытым и игнорируемым проблемам, которые в противном случае были бы представлены пользователю».
Под «иначе представляемое пользователю» Я имею в виду, если вы используете исключение времени выполнения, ленивый программист просто проигнорирует его (вместо того, чтобы перехватить его пустым блоком catch), и пользователь увидит его.
Сводка сводки аргумента такова: «Программисты не будут использовать их должным образом, и их неправильное использование хуже, чем отсутствие их» * .
В этом аргументе есть доля правды, и на самом деле, я подозреваю, что мотивация Гослинга не помещать переопределения операторов в Java исходит из аналогичного аргумента - они вводят программиста в заблуждение, поскольку им часто злоупотребляют.
Но, в конце концов, я нахожу это фиктивным аргументом Хейлсберга и, возможно, постфактумным объяснением недостатка, а не продуманным решением.
Я бы сказал, что, хотя чрезмерное использование проверенных исключений является плохой вещью и ведет к небрежной обработке пользователями, но правильное их использование позволяет программисту API давать большую пользу клиентскому программисту API.
Теперь программист API должен быть осторожен, чтобы не создавать повсеместно проверенные исключения, иначе они просто будут раздражать клиентского программиста. Очень ленивый клиентский программист прибегнет к ловле (Exception) {}
, как предупреждает Хейлсберг, и вся выгода будет потеряна, и ад последует.
Но в некоторых обстоятельствах просто нет замены хорошему проверенному исключению.
Для меня классическим примером является API открытия файлов. Каждый язык программирования в истории языков (по крайней мере, в файловых системах) имеет где-то API, который позволяет открывать файл. И каждый клиентский программист, использующий этот API, знает, что им приходится иметь дело с тем случаем, что файл, который они пытаются открыть, не существует.
Позвольте мне перефразировать это: каждый клиентский программист, использующий этот API , должен знать , что он должен иметь дело с этим делом.
И тут есть одна загвоздка: могут ли программисты API помочь им узнать, что им следует справиться с этим, оставив только комментарии, или они действительно настаивают , что клиент справится с этим.
В Си идиома выглядит примерно так:
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
где fopen
указывает на ошибку, возвращая 0, а C (по глупости) позволяет вам рассматривать 0 как логическое значение и ... В основном, вы изучаете эту идиому и все в порядке. Но что, если ты новичок и ты не выучил идиому. Затем, конечно, вы начинаете с
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
и учиться трудному пути.
Обратите внимание, что здесь речь идет только о строго типизированных языках: есть четкое представление о том, что такое API в строго типизированном языке: это шведский стол функциональности (методов), который вы можете использовать с четко определенным протоколом для каждого один.
Этот четко определенный протокол обычно определяется сигнатурой метода.
Здесь fopen требует, чтобы вы передали ему строку (или символ * в случае C). Если вы дадите ему что-то еще, вы получите ошибку во время компиляции. Вы не следовали протоколу - вы не используете API должным образом.
В некоторых (неясных) языках тип возвращаемого значения также является частью протокола. Если вы попытаетесь вызвать эквивалент fopen()
в некоторых языках без присвоения его переменной, вы также получите ошибку времени компиляции (вы можете сделать это только с помощью функций void).
Я хочу подчеркнуть следующее: На языке со статической типизацией программист API рекомендует клиенту правильно использовать API, предотвращая компиляцию клиентского кода, если он допускает какие-либо очевидные ошибки.
(В динамически типизированном языке, таком как Ruby, вы можете передать что угодно, скажем, float, в качестве имени файла - и он скомпилируется. Зачем беспокоить пользователя проверенными исключениями, если вы даже не собираетесь контролировать метод аргументы. Аргументы, приведенные здесь, применимы только к языкам со статической типизацией.)
Итак, что насчет проверенных исключений?
Вот один из API-интерфейсов Java, который вы можете использовать для открытия файла.
try {
f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
// deal with it. No really, deal with it!
... // this is me dealing with it
}
Видишь этот улов? Вот подпись для этого метода API:
public FileInputStream(String name)
throws FileNotFoundException
Обратите внимание, что FileNotFoundException
является проверено исключение.
Программист API говорит вам следующее:
«Вы можете использовать этот конструктор для создания нового FileInputStream, но вы
a) должен передать имя файла как
Строка
б) должен принять
вероятность того, что файл не может
быть найденным во время выполнения "
И в этом весь смысл.
Ключ в основном состоит в том, что в вопросе говорится как «Вещи, которые находятся вне контроля программиста». Моей первой мыслью было, что он / она имеет в виду то, что находится вне контроля программистов API . Но на самом деле, проверенные исключения при правильном использовании должны действительно относиться к вещам, которые находятся вне контроля как программиста клиента, так и программиста API. Я думаю, что это ключ к тому, чтобы не злоупотреблять проверенными исключениями.
Я думаю, что открытие файла прекрасно иллюстрирует суть. Программист API знает, что вы можете дать им имя файла, которое оказывается несуществующим во время вызова API, и что они не смогут вернуть вам то, что вы хотели, но им придется выбросить исключение. Они также знают, что это будет происходить довольно регулярно и что клиентский программист может ожидать, что имя файла будет правильным во время написания вызова, но это может быть неправильно во время выполнения по независящим от них причинам.
Итак, API делает это явным: в некоторых случаях этот файл не существует, когда вы мне звоните, и вам, черт побери, лучше с этим справиться.
Это было бы яснее с контр-кейсом. Представьте, что я пишу таблицу API. У меня есть модель таблицы где-то с API, включая этот метод:
public RowData getRowData(int row)
Теперь, как программист API, я знаю, что будут случаи, когда некоторый клиент передает отрицательное значение для строки или значение строки вне таблицы. Поэтому у меня может возникнуть желание сгенерировать проверенное исключение и заставить клиента с ним справиться:
public RowData getRowData(int row) throws CheckedInvalidRowNumberException
(я бы на самом деле не назвал это "Проверено".)
Это неправильное использование проверенных исключений. Код клиента будет полон вызовов для извлечения данных строки, каждый из которых должен будет использовать try / catch, и для чего? Собираются ли они сообщить пользователю, что был найден неправильный ряд? Вероятно, нет - потому что, каким бы ни был пользовательский интерфейс, окружающий мое табличное представление, он не должен позволять пользователю переходить в состояние, когда запрашивается недопустимая строка. Так что это ошибка со стороны клиентского программиста.
Программист API все еще может предсказать, что клиент будет кодировать такие ошибки и должен обрабатывать их с исключением времени выполнения, таким как IllegalArgumentException
.
С проверенным исключением в getRowData
, это явно тот случай, который приведет к тому, что ленивый программист Хейлсберга просто добавит пустые уловы. Когда это происходит, недопустимые значения строк не будут очевидны даже для отладчика тестировщика или разработчика клиента, скорее, они приведут к ошибкам, которые трудно определить источник. Ракеты Арианны взорвутся после запуска.
Хорошо, вот в чем проблема: я говорю, что проверенное исключение FileNotFoundException
- это не просто хорошая вещь, а необходимый инструмент в наборе инструментов для программистов API для определения API наиболее полезным способом для программиста клиента. Но CheckedInvalidRowNumberException
- большое неудобство, приводящее к плохому программированию, и его следует избегать. Но как отличить?
Я полагаю, что это не точная наука, и я полагаю, что она лежит в основе и, возможно, в определенной степени оправдывает аргументацию Хейлсберга. Но я не рад выбрасывать ребенка с водой, поэтому позвольте мне извлечь некоторые правила, чтобы отличить хорошие проверенные исключения от плохих:
Вне контроля клиента или Закрыто или Открыто:
Проверенные исключения следует использовать только в том случае, если ошибка не контролируется как API , так и клиентским программистом.
Это связано с тем, как система открыта или закрыта . В ограниченном пользовательском интерфейсе, где клиентский программист имеет контроль, скажем, над всеми кнопками, командами клавиатуры и т. Д., Которые добавляют и удаляют строки из табличного представления (закрытая система), это ошибка программирования клиента, если он пытается извлечь данные из несуществующей строки. В файловой операционной системе, где любое количество пользователей / приложений может добавлять и удалять файлы (открытая система), вполне возможно, что файл, запрашиваемый клиентом, был удален без их ведома, поэтому следует ожидать, что он будет иметь с ним дело. ,
Ubiquity:
Проверенные исключения не должны использоваться при вызове API, который часто выполняется клиентом.
Под частым я имею в виду множество мест в клиентском коде - не часто во времени. Поэтому клиентский код не склонен многократно открывать один и тот же файл, но мое табличное представление получает RowData
повсеместно из разных методов. В частности, я собираюсь написать много кода вроде
if (model.getRowData().getCell(0).isEmpty())
и будет больно каждый раз оборачиваться при попытке / поймать.
Информирование пользователя:
Проверенные исключения следует использовать в тех случаях, когда вы можете представить полезное сообщение об ошибке, которое будет представлено конечному пользователю.
Это «и что вы будете делать, когда это произойдет?» вопрос, который я поднял выше. Это также относится к пункту 1. Поскольку вы можете предсказать, что что-то за пределами вашей системы клиент-API может привести к тому, что файл не будет там, вы можете разумно сообщить об этом пользователю:
"Error: could not find the file 'goodluckfindingthisfile'"
Поскольку ваш неверный номер строки был вызван внутренней ошибкой и не по вине пользователя, на самом деле нет никакой полезной информации, которую вы можете им дать. Если ваше приложение не позволяет исключениям среды выполнения попадать на консоль, оно, вероятно, в конечном итоге выдаст им какое-то ужасное сообщение, например:
"Internal error occured: IllegalArgumentException in ...."
Короче говоря, если вы не думаете, что ваш клиентский программист может объяснить ваше исключение так, чтобы это помогло пользователю, то вам, вероятно, не следует использовать проверенное исключение.
Так что это мои правила. Несколько придумано, и, несомненно, будут исключения (пожалуйста, помогите мне уточнить их, если хотите). Но мой главный аргумент заключается в том, что в таких случаях, как FileNotFoundException
, проверенное исключение является такой же важной и полезной частью контракта API, как и типы параметров. Поэтому мы не должны отказываться от него только потому, что он используется не по назначению.
Извините, я не хотел делать это так долго и вафельно. Позвольте мне закончить с двумя предложениями:
A: Программисты API: экономно используйте проверенные исключения, чтобы сохранить их полезность. В случае сомнений используйте непроверенное исключение.
B: Клиентские программисты: привычка создавать упакованное исключение (Google google) на раннем этапе разработки. JDK 1.4 и более поздние версии предоставляют конструктор в RuntimeException
для этого, но вы также можете легко создать и свой собственный. Вот конструктор:
public RuntimeException(Throwable cause)
Тогда приобретайте привычку всякий раз, когда вам нужно обработать проверенное исключение, и вы чувствуете себя ленивым (или вы думаете, что программист API слишком переусердствовал в использовании проверенного исключения в первую очередь), не просто глотайте исключение , заверните и выбросьте.
try {
overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
throw new RuntimeException(exception);
}
Поместите это в один из маленьких шаблонов кода вашей IDE и используйте его, когда вам лень. Таким образом, если вам действительно нужно обработать проверенное исключение, вы будете вынуждены вернуться и разобраться с ним, увидев проблему во время выполнения. Потому что, поверьте мне (и Андерсу Хейлсбергу), вы никогда не вернетесь к этому TODO в своем
catch (Exception e) { /* TODO deal with this at some point (yeah right) */}