Разработка языка с проверенными исключениями - PullRequest
7 голосов
/ 09 февраля 2010

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

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

Возможно, может быть необязательным перехватывать исключения, и, следовательно, если кто-то не будет пойман, произойдет сбой программы? Или, может быть, могут существовать специальные обозначения, обозначающие, что исключение не обрабатывается (например, обозначение виртуальной функции C ++ '= 0')? Или я мог бы даже вызвать сбой программы, если обработчик исключений пуст (хотя это может удивить новичков в программировании)?

Как насчет синтаксиса try ... catch, как вы думаете, может быть более краткий способ выражения того, что исключение перехватывается? Язык будет использовать сборщик мусора, очень похожий на Java, так есть ли необходимость в предложении finally? Наконец, какие еще недостатки существуют у проверенных исключений и какие существуют потенциальные решения (если таковые имеются)?

Ответы [ 7 ]

13 голосов
/ 09 февраля 2010

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

Это не так для Java сегодня (особенно если вы представляете себе Java - без проверенных исключений).

Если у вас есть такая подпись в Java:

Foo bar(Baz)

он говорит: "Я беру Baz в качестве ввода и выдаю Foo в качестве вывода". Но это ложь.

На самом деле, bar принимает либо a Baz или null в качестве ввода. Он также принимает полное глобальное состояние, состояние класса и состояние экземпляра в качестве входных данных, а также весь юниверс (например, через файловый ввод-вывод, сетевой ввод-вывод, ввод-вывод базы данных и т. Д.). ). И он не выдает Baz в качестве вывода: он выдает или a Foo или null или исключение или Bottom (т.е. вообще ничего). Плюс его выходные данные также включают в себя все глобальное состояние, все состояние класса, все состояние экземпляра и в действительности также все состояние вселенной.

bar s фактический тип:

(IO<Foo | null | Exception> | Bottom) bar(IO<Baz | null>)

или что-то в этом роде.

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

В любом случае, именно поэтому я считаю, что общая идея за Checked Exceptions - хорошая вещь & trade;, даже если конкретная реализация в Java может быть немного громоздкой.

Как исправить отмеченные исключения во многом зависит от того, что именно вы думаете на самом деле неправильно с ними.

Некоторые люди считают, что проблема с Проверяемыми Исключениями заключается в том, что когда вы меняете внутреннюю реализацию вашего метода на использование другого вспомогательного метода, чем он делал раньше, который генерирует другой набор исключений, чем старый, вам нужно либо явно обрабатывать эти исключения или объявлять их, тем самым нарушая работу всех ваших клиентов. Теперь, если вы считаете, что это неправильно с Checked Exceptions, то есть только один способ их исправить: не создавать их в первую очередь. Изменение исключений, которые вы генерируете , является критическим изменением в вашем контракте API, а нарушение изменений в вашем контракте API должно нарушить код клиента. (Или, точнее: вам не следует вносить критические изменения в ваш контракт API, чтобы не нарушать код клиента.)

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

С Checked Exceptions, как реализовано в Java, каждый фрагмент кода между также должен быть изменен.

Одним из предложений по решению этой проблемы являются Привязанные объявления об исключениях . (Улучшено в Модульных объявленных исключениях .)

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

Допустим, у вас есть метод чтения файлов, который делегирует другой метод:

String fileReader(String filename) {
  return this.fileHelper.read(filename);
}

Теперь вы заходите в JavaDoc для FileHelper#read и вырезаете и вставляете список исключений в свой метод:

String fileReader(String filename) throws IOException, CustomFileReaderEx

Теперь автор FileHelper#read решает, что он использует другую стратегию реализации.Теперь он удостоверится, что чтение файла фактически никогда не может завершиться неудачей, сначала убедившись, что файл существует, может быть открыт и имеет правильный формат.Так что, естественно, набор исключений меняется.Больше невозможно получить IOException или CustomFileReaderEx.Вместо этого вы можете получить InvalidFilenameEx или CorruptDataEx.Итак, вы снова вырезаете и вставляете:

String fileReader(String filename) throws InvalidFilenameEx, CorruptDataEx

Не только вам нужно было внести это изменение, но и всем, кто звонит fileReader (и всем, кто звонит им, и всем, кто звонит ими ...) также.Это безумие!Причина, по которой вы в первую очередь делегировали вызов fileHelper, заключалась в том, что вам не нужно беспокоиться об этих деталях.

Итак, идея якорных объявлений исключений состоит в том, чтобы использовать это делегирование для исключения.Сами декларации.Вместо того чтобы говорить, какие точные исключения вы выбрасываете, вы просто обвиняете кого-то другого.«Он сделал это!»:

String fileReader(String filename) throws like this.fileHelper.read

А ваши клиенты просто говорят:

Foo whatever() throws like fileReader

Таким образом, когда FileHelper меняет свои исключения, тогда только код, который нужно изменить, - это код обработки исключений самого верхнего уровня, как я описал выше для случая unchecked .

Конечно, существуют ограничения.Например, чтобы не нарушать инкапсуляцию, вы можете использовать только те идентификаторы в предложении throws like, которые доступны всем вашим клиентам.Если в этом случае fileHelper является полем private, вы не можете его использовать.Тебе нужен другой путь.Например, если класс FileHelper равен public (или это частный пакет и все ваши клиенты живут в одном пакете), вы можете вместо этого сказать

String fileReader(String filename) throws like FileHelper.read

Есть и другие ограничения, перечисленные в документе.(Один из них поднят в документе «Модульные объявления привязанных исключений».)

В любом случае, это один способ решить некоторые проблемы с проверенными исключениями.Тем не менее, Проверенные исключения существуют уже почти 40 лет, и мы до сих пор не выяснили их, так что это очевидно трудная проблема.

2 голосов
/ 09 февраля 2010

Я думаю, вам следует рассмотреть возможность использования монады с тремя состояниями, такой как тип Lift's Box для обработки ошибок. Тогда вам вообще не нужно будет использовать исключения.

http://github.com/dpp/liftweb/blob/master/framework/lift-base/lift-common/src/main/scala/net/liftweb/common/Box.scala

2 голосов
/ 09 февраля 2010

Думаю, вам следует прочитать пост Нила Гафтерса об улучшенной обработке исключений в jdk7 и интервью с Андерсом Хьельсбергом об обработке исключений в C # (против Java)

1 голос
/ 09 февраля 2010

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

FWIW, я согласен - по большому счету, они хорошая вещь (тм). Плохие привычки программистов по использованию функции не обязательно означают, что функция плохая. Я думаю, что в большинстве случаев программисты не понимают, что они могут взять исключение и поднять его до нуля.

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

Java имеет эту функцию (RuntimeException, а ее подклассы являются непроверенными исключениями).

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

У Java это есть; предложение throws в объявлении метода.

Как насчет синтаксиса try ... catch, как вы думаете, может быть более краткий способ выражения того, что исключение перехватывается?

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

Однако у меня есть пара предложений для try..catch:

catch..from

Это то, что я давно хотел в Java и родственных языках (действительно нужно правильно написать это и отправить JSR): catch...from.

Вот пример:

FileOutputStream    output;
Socket              socket;
InputStream         input;
byte[]              buffer;
int                 count;

// Not shown: Opening the input and output, allocating the buffer,
// getting the socket's input stream

try
{
    while (/* ... */)
    {
        count = input.read(buffer, 0, buffer.length);
        output.write(buffer, 0, count);

        // ...
    }
}
catch (IOException ioe from input)
{
    // Handle the error reading the socket
}
catch (IOException ioe from output)
{
    // Handle the error writing to the file
}

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

Это то, что может сделать с существующими механизмами и строгими правилами кодирования, но это очень сложно.

Несколько выражений перехвата в одном блоке

Запланированные улучшения JDK7.

Retry

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

try
{
    // ...stuff here...

    if (condition)
    {
        foo.doSomething();
    }

    // ...stuff here...
}
catch (SomeException se)
{
    if (foo.takeRemedialAction() == Remedy.SUCCESS)
    {
        retry;
    }

    // ...handle exception normally...
}

Здесь doSomething может выйти из строя исключительным образом, но таким образом, что takeRemedialAction может исправить. Это продолжает тему исключения исключительных условий из основной логики. Естественно, retry возвращает выполнение обратно к неудачной операции, которая может быть глубиной doSomething или какого-то другого подметода, который она вызывает. Вы понимаете, что я имею в виду, бросая вызов.

Команде гораздо проще работать с существующими механизмами: просто создайте подпрограмму, которая doSomething плюс takeRemedialAction в порядке исключения, и вместо этого поместите этот вызов в логику основной линии. Итак, этот находится внизу списка, но эй ...

0 голосов
/ 15 января 2011

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

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

0 голосов
/ 15 февраля 2010

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

Что касается синтаксиса, то есть замечательная статья Ника Бентона и Эндрю Кеннеди , в которой объясняются слабые стороны стандартного синтаксиса и предлагаются новые убедительные варианты. Любой, кто занимается дизайном в этой области, должен прочитать это.

0 голосов
/ 09 февраля 2010

Я не вижу другого способа связать блок кода возможными ошибками. Я думаю, что блок try .. catch является последовательным. В качестве усовершенствования в Ruby есть ключевое слово, которое позволяет повторить неудачный блок кода, я думаю, это хорошее улучшение

...