Проверено против Не проверено против Без исключения ... Лучшая практика противоречивых убеждений

10

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

Требования к исключениям (в произвольном порядке):

  1. Документирование : язык должен иметь возможность документировать исключения, которые может генерировать API. В идеале этот носитель документации должен быть пригоден для использования машиной, чтобы компиляторы и IDE могли оказывать поддержку программисту.

  2. Передача исключительных ситуаций : эта ситуация очевидна, чтобы позволить функции передавать ситуации, которые мешают вызываемой функциональности выполнить ожидаемое действие. На мой взгляд, есть три большие категории таких ситуаций:

    2.1 Ошибки в коде, которые приводят к тому, что некоторые данные являются недействительными.

    2.2 Проблемы с настройкой или другими внешними ресурсами.

    2.3 Ресурсы, которые по своей природе ненадежны (сеть, файловые системы, базы данных, конечные пользователи и т. Д.). Это небольшой случай, так как их ненадежный характер заставляет нас ожидать их спорадических неудач. В этом случае эти ситуации следует считать исключительными?

  3. Предоставьте достаточно информации для кода, чтобы справиться с ним : Исключения должны предоставлять достаточную информацию для вызываемого, чтобы он мог реагировать и, возможно, справляться с ситуацией. информация также должна быть достаточной, чтобы при регистрации эти исключения обеспечили программисту достаточный контекст, чтобы идентифицировать и изолировать ошибочные операторы и предоставить решение.

  4. Обеспечить уверенность программиста в текущем состоянии состояния выполнения его кода : возможности обработки исключений в программной системе должны присутствовать достаточно, чтобы обеспечить необходимые меры безопасности, оставаясь в стороне от программиста, чтобы он мог сосредоточиться на задаче в рука.

Чтобы покрыть это, были реализованы следующие методы на разных языках:

  1. Проверенные исключения Обеспечивают отличный способ документировать исключения, и теоретически при правильной реализации должны обеспечивать достаточную уверенность в том, что все в порядке. Однако стоимость такова, что многие считают более продуктивным просто обходить либо проглатывая исключения, либо перебрасывать их как непроверенные исключения. При ненадлежащим образом проверенных исключениях все это теряет свою полезность. Кроме того, проверенные исключения затрудняют создание API, стабильного во времени. Реализации универсальной системы в конкретном домене принесут массу исключительных ситуаций, которые будет сложно поддерживать с использованием исключительно проверенных исключений.

  2. Непроверенные исключения - гораздо более универсальные, чем проверенные исключения, они не могут должным образом документировать возможные исключительные ситуации данной реализации. Они полагаются на специальную документацию, если вообще. Это создает ситуации, когда ненадежный характер среды маскируется API, который создает видимость надежности. Кроме того, когда эти исключения выбрасываются, они теряют свое значение по мере продвижения вверх через уровни абстракции. Так как они плохо документированы, программист не может нацеливаться на них конкретно, и ему часто приходится создавать гораздо более широкую сеть, чем необходимо, чтобы вторичные системы, если они выйдут из строя, не сломали всю систему. Что возвращает нас к проблеме глотания проверенных исключений.

  3. Типы возвращаемых данных с несколькими состояниями Здесь необходимо полагаться на непересекающийся набор, кортеж или другую подобную концепцию для возврата ожидаемого результата или объекта, представляющего исключение. Здесь нет разматывания стека, нет вырезки кода, все выполняется нормально, но возвращаемое значение должно быть проверено на наличие ошибок перед продолжением. На самом деле я еще не работал с этим, поэтому не могу прокомментировать из своего опыта. Я признаю, что он разрешает некоторые проблемы, исключая обход нормального потока, но все же он будет страдать от тех же проблем, что и проверенные исключения, так как он утомителен и постоянно "перед вами".

Итак, вопрос:

Каков ваш опыт в этом вопросе и что, по вашему мнению, является лучшим кандидатом для создания хорошей системы обработки исключений для языка?


РЕДАКТИРОВАТЬ: Через несколько минут после написания этого вопроса я наткнулся на этот пост , жуткий!

Newtopian
источник
2
«он будет страдать от тех же проблем, что и проверенные исключения, так как он утомителен и постоянно перед вами»: не совсем: при правильной языковой поддержке вам нужно всего лишь запрограммировать «путь успеха», а базовый языковой механизм позаботится о распространении ошибки.
Джорджио
«Язык должен иметь среднее значение для исключения задокументировать API может бросить.» - weeeel. В C ++ «мы» узнали, что это на самом деле не работает. Все, что вы действительно можете сделать, это указать, может ли API генерировать какие-либо исключения. (Это действительно короткая история, но я думаю, что просмотр noexceptистории на C ++ может дать очень хорошее представление о EH в C # и Java.)
Мартин Ба,

Ответы:

10

В первые дни C ++ мы обнаружили, что без какого-либо общего программирования языки со строгой типизацией были бы чрезвычайно громоздкими. Мы также обнаружили, что проверенные исключения и общее программирование не работали вместе, и проверенные исключения были по существу заброшены.

Типы возврата Multiset хороши, но не заменит исключения. Без исключений, код полон проверочного шума.

Другая проблема с проверенными исключениями состоит в том, что изменение в исключениях, генерируемых функцией низкого уровня, вызывает каскад изменений во всех вызывающих, их вызывающих и т. Д. Единственный способ предотвратить это - для каждого уровня кода перехватывать любые исключения, создаваемые более низкими уровнями, и заключать их в новое исключение. Опять же, вы получите очень шумный код.

Кевин Клайн
источник
2
Обобщения помогают решить целый класс ошибок, которые в основном связаны с ограничением поддержки языка в парадигме ОО. тем не менее, альтернативы, похоже, либо имеют код, который в основном выполняет проверку ошибок, либо работает, надеясь, что ничего не случится. Либо у вас постоянно возникают необычные ситуации, либо вы живете в стране грез пушистых белых кроликов, которые становятся ужасно уродливыми, когда вы бросаете большого плохого волка в середину!
Newtopian
3
+1 за каскадную проблему. Любая система / архитектура, которая усложняет изменение, приводит только к «мартовским» и грязным системам, независимо от того, насколько хорошо разработанные авторы думали, что они были.
Матье М.
2
@Newtopian: шаблоны делают вещи, которые невозможно выполнить в строгой объектной ориентации, например, обеспечивают безопасность статического типа для универсальных контейнеров.
Дэвид Торнли
2
Я хотел бы видеть систему исключений с понятием «проверенные исключения», но очень отличную от Java. Проверено-Несс не должна быть атрибутом исключения типа , а бросить сайты, поймать сайты, а также случаи исключения; если метод объявляется как выбрасывающий проверенное исключение, это должно иметь два эффекта: (1) функция должна обрабатывать «выбрасывание» проверенного исключения, делая что-то особенное по возвращению (например, устанавливая флаг переноса и т. д. в зависимости от точная платформа), к которому должен быть подготовлен вызывающий код.
суперкат
7
«Без исключений код полон шума проверки ошибок». Я не уверен в этом: в Haskell вы можете использовать монады для этого, и весь шум проверки ошибок исчез. Шум, создаваемый «типами возвращаемых данных с несколькими состояниями», является скорее ограничением языка программирования, чем самого решения.
Джорджио
9

В течение долгого времени в ОО-языках использование исключений являлось стандартом де-факто для сообщения об ошибках. Но функциональные языки программирования предоставляют возможность другого подхода, например, использование монад (которые я не использовал) или более легкое «железнодорожное программирование», как описано Скоттом Влащиным.

Это действительно вариант многогосударственного типа результата.

  • Функция возвращает либо успех, либо ошибку. Он не может вернуть оба (как в случае с кортежем).
  • Все возможные ошибки были кратко документированы (по крайней мере, в F # с типами результатов в качестве различимых объединений).
  • Вызывающая сторона не может использовать результат без учета того, был ли результат успешным или неудачным.

Тип результата может быть объявлен так

type Result<'TSuccess,'TFail> =
| Success of 'TSuccess
| Fail of 'TFail

Таким образом , в результате функции , которая возвращает этот тип будет либо Successили Failтипа. Это не может быть и то и другое.

В более императивно-ориентированных языках программирования этот стиль может потребовать большого количества кода на сайте вызывающей стороны. Но функциональное программирование позволяет вам создавать связывающие функции или операторы, чтобы связывать воедино несколько функций, чтобы проверка ошибок не занимала половину кода. Например:

// Create an updateUser function that takes an id, and new state
// as input, and updates an existing user.
let updateUser id input =
    validateInput input
    >>= loadUser id
    >>= updateUser input
    >>= saveUser id
    >>= notifyAboutUserUpdated

updateUserВызовы функций каждой из этих функций последовательно, и каждый из них может не сработать. Если все они успешны, возвращается результат последней вызванной функции. В случае сбоя одной из функций, результат этой функции будет результатом всей updateUserфункции. Все это обрабатывается пользовательским оператором >> =.

В приведенном выше примере типы ошибок могут быть

type UserValidationErrorType =
| InvalidEmail of string
| MissingFirstName of string
... etc

type DbErrorType =
| RecordNotFound of int
| ConcurrencyError of int

type UpdateUserErrorType =
| InvalidInput of UserValidationErrorType
| DbError of DbErrorType

Если вызывающая updateUserсторона явно не обрабатывает все возможные ошибки из функции, компилятор выдаст предупреждение. Итак, у вас есть все задокументировано.

В Haskell есть doнотация, которая может сделать код еще чище.

Пит
источник
2
Очень хороший ответ и ссылки (железнодорожное программирование), +1. Вы можете упомянуть doнотацию на Haskell , которая делает полученный код еще чище.
Джорджио
1
@ Джорджио - я сделал это сейчас, но я не работал с Haskell, только F #, поэтому я не мог написать об этом много. Но вы можете добавить к ответу, если хотите.
Пит
Спасибо, я написал небольшой пример, но так как он не был достаточно маленьким, чтобы добавить его к вашему ответу, я написал полный ответ (с некоторой дополнительной справочной информацией).
Джорджио
2
Это Railway Oriented Programmingточно монадическое поведение.
Daenyth
5

Я нахожу ответ Пита очень хорошим, и я хотел бы добавить некоторые соображения и один пример. Очень интересная дискуссия относительно использования исключений в сравнении с возвращением специальных значений ошибок может быть найдена в Программировании в Standard ML Робертом Харпером в конце Раздела 29.3, стр. 243, 244.

Проблема заключается в реализации частичной функции, fвозвращающей значение некоторого типа t. Одним из решений является использование функции типа

f : ... -> t

и бросить исключение, когда нет возможного результата. Второе решение заключается в реализации функции с типом

f : ... -> t option

и вернуть SOME vна успех, и NONEна провал.

Вот текст из книги с небольшой адаптацией, сделанной мной, чтобы сделать текст более общим (книга ссылается на конкретный пример). Измененный текст написан курсивом .

Каковы компромиссы между двумя решениями?

  1. Решение, основанное на типах опций, делает явным в типе функции fвозможность сбоя. Это вынуждает программиста явно проверять наличие ошибок, анализируя случай результата вызова. Проверка типа гарантирует, что никто не может использовать t optionтам, гдеt ожидается. Решение, основанное на исключениях, явно не указывает на сбой в своем типе. Тем не менее, программист, тем не менее, вынужден обрабатывать ошибки, так как в противном случае ошибка неисследованного исключения возникнет во время выполнения, а не во время компиляции.
  2. Решение, основанное на типах опций, требует явного анализа случая по результатам каждого вызова. Если «большинство» результатов успешны, проверка является излишней и, следовательно, чрезмерно дорогостоящей. Решение, основанное на исключениях, освобождается от этих издержек: оно смещено в сторону «нормального» случая возврата a t, а не в случае «отказа», когда результат вообще не возвращается . Реализация исключений гарантирует, что использование обработчика более эффективно, чем явный анализ случая, в случае, если сбой редок по сравнению с успехом.

[cut] В целом, если эффективность имеет первостепенное значение, мы склонны отдавать предпочтение исключениям, если отказ является редкостью, и предпочитаем варианты, если отказ является относительно распространенным явлением. Если, с другой стороны, статическая проверка имеет первостепенное значение, тогда выгодно использовать опции, поскольку средство проверки типов будет обеспечивать выполнение требования о том, чтобы программист проверял наличие ошибок, а не возникало, когда ошибка возникала только во время выполнения.

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

Что касается идеи, что представление ошибки в возвращаемом типе приводит к проверке ошибок, распространяющейся по всему коду: это не должно иметь место. Вот небольшой пример в Haskell, который иллюстрирует это.

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

import Text.Read

parseInt :: String -> Maybe Int
parseInt s = readMaybe s :: Maybe Int

safeDiv :: Int -> Int -> Maybe Int
safeDiv n d = if d /= 0 then Just (n `div` d) else Nothing

toString :: Maybe Int -> String
toString (Just i) = show i
toString Nothing  = "error"

main = do
         -- Get two lines from the terminal.
         nStr <- getLine
         dStr <- getLine

         -- Parse each string and divide.
         let r = do n <- parseInt nStr
                    d <- parseInt dStr
                    safeDiv n d

         -- Print the result.
         putStrLn $ toString r

Разбор и деление выполняются в let ...блоке. Обратите внимание, что при использовании Maybeмонады и doнотации указывается только путь успеха : семантика Maybeмонады неявно распространяет значение ошибки ( Nothing). Никаких накладных расходов для программиста.

Джорджио
источник
2
Я думаю, что в таких случаях, когда вы хотите напечатать какое-то полезное сообщение об ошибке, Eitherтип будет более подходящим. Что вы делаете, если попадаете Nothingсюда? Вы просто получаете сообщение «ошибка». Не очень полезно для отладки.
Сара
1

Я стал большим поклонником Проверенных Исключений, и я хотел бы поделиться своим общим правилом о том, когда их использовать.

Я пришел к выводу, что в основном мой код имеет дело с двумя типами ошибок. Есть ошибки, которые можно проверить перед выполнением кода, и есть ошибки, которые нельзя проверить перед выполнением кода. Простой пример ошибки, которую можно проверить перед выполнением кода в исключении NullPointerException.

//... bad code below.  the runnable variable
// tries to call the run() method before the variable
// is instantiated.  Running the code below will cause
// a NullPointerException.
Runnable runnable = null;
runnable.run();

Простой тест мог бы избежать ошибки, такой как ...

Runnable runnable = null;
...
if (runnable != null)
{   runnable.run(); }

В вычислительной технике бывают случаи, когда вы можете выполнить 1 или более тестов перед выполнением кода, чтобы убедиться, что вы в безопасности и вы все равно получите исключение. Например, вы можете проверить файловую систему, чтобы убедиться, что на жестком диске достаточно места, прежде чем записывать данные на диск. В многопроцессорной операционной системе, такой как используемая сегодня, ваш процесс может проверить дисковое пространство, и файловая система вернет значение, сообщающее, что места достаточно, а затем переключение контекста на другой процесс может записать оставшиеся байты, доступные для операционной системы. система. Когда контекст операционной системы переключается обратно на ваш работающий процесс, где вы записываете свое содержимое на диск, возникает исключение просто потому, что в файловой системе недостаточно места на диске.

Я рассматриваю сценарий выше как идеальный случай для Проверенного Исключения. Это исключение в коде, которое заставляет вас иметь дело с чем-то плохим, даже если ваш код может быть идеально написан. Если вы решили делать плохие вещи, такие как «проглотить исключение», вы плохой программист. Кстати, я обнаружил случаи, когда разумно проглотить исключение, но, пожалуйста, оставьте комментарий в коде, почему исключение было проглочено. Механизм обработки исключений не виноват. Я часто шучу, что предпочитаю, чтобы мой кардиостимулятор был написан на языке с проверенными исключениями.

Есть моменты, когда становится трудно решить, является ли код тестируемым или нет. Например, если вы пишете интерпретатор и выдается исключение SyntaxException, когда код не выполняется по какой-либо синтаксической причине, должно ли SyntaxException быть проверенным исключением или (в Java) RuntimeException? Я бы ответил, если интерпретатор проверяет синтаксис кода перед его выполнением, тогда Исключение должно быть RuntimeException. Если интерпретатор просто запускает код «горячий» и просто вызывает синтаксическую ошибку, я бы сказал, что исключение должно быть проверенным исключением.

Я признаю, что я не всегда счастлив поймать или выбросить Проверенное Исключение, потому что есть время, когда я не уверен, что делать. Проверенные исключения - это способ заставить программиста помнить о потенциальной проблеме, которая может возникнуть. Одна из причин, по которой я программирую на Java, заключается в том, что в нем есть Проверенные исключения.

Джеймс Мольер
источник
1
Я бы предпочел, чтобы мой кардиостимулятор был написан на языке, который вообще не имел исключений, и все строки кода обрабатывали ошибки с помощью кодов возврата. Когда вы генерируете исключение, вы говорите «все пошло не так», и единственный безопасный способ продолжить обработку - это остановить и перезапустить. Программа, которая так легко попадает в недопустимое состояние, - это не то, что вам нужно для критически важного программного обеспечения (а Java явно запрещает его использование для критически важного программного обеспечения в EULA)
gbjbaanb
Используя исключение и не проверяя их, используя код возврата и не проверяя их в конце, все это приводит к одинаковой остановке сердца.
Newtopian
-1

В настоящее время я нахожусь в середине довольно большого проекта / API на основе ООП, и я использовал эту схему исключений. Но все зависит от того, насколько глубоко вы хотите использовать обработку исключений и тому подобное.

ExpectedException
- AuthorisedException
- EmptySetException
- NoRemainingException
- NoRowsException
- NotFoundException
- ValidationException

Неожиданное исключение
- ConnectivityException
- EnvironmentException
- ProgrammerException
- SQLException

ПРИМЕР

   $valid_types = array('mysql', 'oracle', 'sqlite');
       if (!in_array($type, $valid_types)) {
           throw new ecProgrammerException(
        'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
    $type,
    join(', ', $valid_types)
    );
}
MattyD
источник
11
Если ожидается исключение, оно на самом деле не является исключением. "NoRowsException"? Похоже, поток управления для меня, и, следовательно, плохое использование исключения.
Квентин Старин
1
@qes: имеет смысл вызывать исключение всякий раз, когда функция не может вычислить значение, например, double Math.sqrt (double v) или User findUser (long id). Это дает вызывающей стороне свободу отлавливать и обрабатывать ошибки там, где это удобно, вместо проверки после каждого вызова.
Кевин Клайн
1
Ожидаемый = поток управления = анти-образец исключения. Исключение не должно использоваться для потока управления. Если ожидается, что будет выдана ошибка для конкретного ввода, то она просто будет передана как часть возвращаемого значения. Итак, у нас есть NANили NULL.
Эонил
1
@Eonil ... или Option <T>
Мартен Бодьюс