Зачем разрабатывать современный язык без механизма обработки исключений?

47

Многие современные языки предоставляют богатые функции обработки исключений , но язык программирования Apple Swift не предоставляет механизм обработки исключений .

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

Как, например, я мог бы лучше выразить что-то вроде

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

в языке (например, Swift), в котором отсутствует обработка исключений?

Оромэ
источник
11
Вы можете добавить Go в список , если мы просто проигнорируем, panicчто не совсем то же самое. В дополнение к тому, что там сказано, исключение - это не более чем сложный (но удобный) способ выполнения GOTO, хотя никто не называет это так по понятным причинам.
JensG
3
Ответ на ваш вопрос в том, что вам нужна языковая поддержка для исключений, чтобы их написать. Языковая поддержка обычно включает управление памятью; поскольку исключение можно генерировать где угодно и перехватывать где угодно, должен быть способ избавиться от объектов, которые не зависят от потока управления.
Роберт Харви
1
Я не совсем слежу за тобой, @ Роберт. C ++ удается поддерживать исключения без сборки мусора.
Карл Билефельдт
3
@KarlBielefeldt: На большие деньги, насколько я понимаю. Опять же, есть ли что- то, что предпринимается в C ++ без больших затрат, по крайней мере, с усилиями и необходимыми знаниями предметной области?
Роберт Харви
2
@RobertHarvey: Хороший вопрос. Я один из тех, кто не задумывается об этих вещах. Я думал, что ARC - это GC, но, конечно, это не так. Таким образом, в принципе (если я понимаю, грубо), исключения были бы грязной вещью (несмотря на C ++) в языке, где удаление объектов полагалось на поток управления?
Оромэ

Ответы:

33

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

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

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

Я не знаком со Swift, но то, что я читал об обработке ошибок, показывает, что они предназначены для обработки ошибок в соответствии с более функциональными шаблонами. Я видел примеры кода с successи failureблоки , которые очень похожи на фьючерсы.

Вот пример с использованием Futureот этого Scala учебника :

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

Вы можете видеть, что он имеет примерно ту же структуру, что и ваш пример с использованием исключений. futureБлок подобно try. onFailureБлок , как обработчик исключений. В Scala, как и в большинстве функциональных языков, Futureреализован полностью с использованием самого языка. Он не требует специального синтаксиса, как это делают исключения. Это означает, что вы можете определить свои собственные аналогичные конструкции. Может быть, добавить timeoutблок, например, или функциональность регистрации.

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

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

Это те вещи, которые вы «получаете, теряя исключения».

Карл Билефельдт
источник
1
Так Futureчто, по сути, это способ проверки значения, возвращаемого функцией, не останавливаясь для его ожидания. Как и Swift, он основан на возвращаемом значении, но в отличие от Swift, ответ на возвращаемое значение может произойти позже (немного похоже на исключения). Правильно?
Оромэ
1
Вы понимаете Futureправильно, но я думаю, что вы, возможно, неправильно характеризуете Свифта. Посмотрите первую часть этого ответа stackoverflow , например.
Карл Билефельдт
Хм, я новичок в Swift, поэтому этот ответ мне сложно разобрать. Но если я не ошибаюсь: это, по сути, передает обработчик, который может быть вызван позднее; правильно?
Оромэ
Да. Вы в основном создаете обратный вызов при возникновении ошибки.
Карл Билефельдт
Eitherбудет лучшим примером ИМХО
Павел Прагак
19

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

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

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

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

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

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

Rufflewind
источник
Правильно ли то, что система типов, которой обладает Swift, включая опциональные, является своего рода «мощной системой типов», которая достигает этого?
Оромэ
2
Да, дополнительные функции и, в более общем смысле, типы сумм (называемые в Swift / Rust "enum") могут достичь этого. Однако для их приятного использования требуется дополнительная работа: в Swift это достигается с помощью необязательного синтаксиса цепочки; в Хаскеле это достигается с помощью монадической дотации.
Rufflewind
Может ли «достаточно мощная система типов» дать трассировку стека, если нет, то она довольно бесполезна
Павел Прашак,
1
+1 за указание на то, что исключения скрывают поток контроля. Так же, как вспомогательное примечание: само собой разумеется, что исключения на самом деле не являются более злыми, чем goto: The gotoограничен одной функцией, которая является довольно небольшим диапазоном, при условии, что функция действительно мала, исключение действует скорее как некоторый крест gotoи come from(см. en.wikipedia.org/wiki/INTERCAL ;-)). Он может в значительной степени соединить любые две части кода, возможно, пропуская код некоторых третьих функций. Единственное, что он не может сделать, что gotoможет, это вернуться назад.
Мастер
2
@ PawełPrażak При работе с большим количеством функций более высокого порядка трассировка стека не так важна. Сильные гарантии относительно входов и выходов и избежание побочных эффектов - вот что предотвращает эту путаницу, приводящую к запутанным ошибкам.
Джек,
11

Здесь есть несколько хороших ответов, но я думаю, что одна важная причина не была подчеркнута достаточно: когда возникают исключения, объекты могут быть оставлены в недопустимых состояниях. Если вы можете «перехватить» исключение, то ваш код обработчика исключений сможет получить доступ к этим недопустимым объектам и работать с ними. Это будет ужасно неправильно, если код для этих объектов не будет написан идеально, что очень и очень трудно сделать.

Например, представьте себе реализацию Vector. Если кто-то создает экземпляр Vector с набором объектов, но во время инициализации возникает исключение (например, при копировании ваших объектов во вновь выделенную память), очень трудно правильно кодировать реализацию Vector таким образом, чтобы память просочилась. Эта короткая статья Stroustroup охватывает пример вектора .

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

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

Чарли Флауэрс
источник
1
цель улова - сделать объект согласованным (или завершить что-либо, если это невозможно) после возникновения ошибки.
JoulinRouge
@JoulinRouge Я знаю. Но некоторые языки решили не дать вам такую ​​возможность, а вместо этого прервать весь процесс. Те языковые дизайнеры знают, какую очистку вы хотели бы сделать, но пришли к выводу, что слишком сложно дать вам это, и компромиссы, вовлеченные в это, не будут стоить этого. Я понимаю, что вы можете не согласиться с их выбором ... но важно знать, что они сделали выбор сознательно по этим конкретным причинам.
Чарли Флауэрс
6

Изначально я был удивлен языком Rust: он не поддерживает исключения catch. Вы можете генерировать исключения, но только среда выполнения может их перехватить, когда задача (поток обработки, но не всегда отдельный поток ОС) умирает; если вы начинаете задание самостоятельно, то можете спросить, нормально ли оно вышло или выполнило его fail!().

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

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

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}
o11c
источник
1
Так что справедливо ли сказать, что это тоже (как и подход Го) похоже на Swift, который имеет assert, но нет catch?
Оромэ
1
В Swift попробуйте! означает: да, я знаю, что это может произойти сбой, но я уверен, что это не будет, поэтому я не обрабатываю это, и если это не удается, то мой код неверен, поэтому, пожалуйста, в этом случае произойдет сбой.
gnasher729
6

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

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

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

Это на самом деле означает «неожиданный».

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

Простой пример: API хеш-карты может иметь два метода:

Value get(Key)

а также

Option<Value> getOption(key)

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

Кодекс никогда не должен пытаться справиться с ошибочными базовыми предположениями.

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

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

Поймать блоки повсюду - очень плохая идея.

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

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

Это и некоторое недопонимание того, для чего они на самом деле хороши.

Имитация их путем выполнения ВСЕХ через монадическое связывание делает ваш код менее читаемым и обслуживаемым, и вы в конечном итоге не получаете трассировки стека, что делает этот подход НАМНОГО хуже.

Обработка ошибок в функциональном стиле отлично подходит для ожидаемых ошибок.

Пусть обработка исключений автоматически позаботится обо всем остальном, вот для чего это :)

йомен
источник
3

Свифт использует те же принципы, что и Objective-C, но более последовательно. В Objective-C исключения указывают на ошибки программирования. Они не обрабатываются, кроме как с помощью инструментов отчетности о сбоях. «Обработка исключений» выполняется путем исправления кода. (Есть несколько исключений. Например, в межпроцессном взаимодействии. Но это довольно редко, и многие люди никогда не сталкиваются с этим. А в Objective-C фактически есть try / catch / finally / throw, но вы редко используете их). Swift только что убрал возможность ловить исключения.

Swift имеет функцию, которая выглядит как обработка исключений, но является просто принудительной обработкой ошибок. Исторически Objective-C имел довольно распространенный шаблон обработки ошибок: метод либо возвращал BOOL (YES для успеха), либо ссылку на объект (nil для ошибки, не nil для успеха) и имел параметр «указатель на NSError *» который будет использоваться для хранения ссылки NSError. Swift автоматически преобразует вызовы такого метода в нечто похожее на обработку исключений.

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

gnasher729
источник
Для Свифта это правильный ответ, ИМО. Swift должен оставаться совместимым с существующими системами Objective-C, поэтому у них нет традиционных исключений. Недавно я написал сообщение в блоге о том, как ObjC работает для обработки ошибок: orangejuiceliberationfront.com/…
uliwitness
2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

В C вы бы в конечном итоге что-то вроде выше.

Есть ли вещи, которые я не могу сделать в Swift, которые я мог бы делать с исключениями?

Нет, ничего нет Вы просто обрабатываете коды результатов, а не исключения.
Исключения позволяют вам реорганизовать ваш код так, чтобы обработка ошибок была отделена от вашего счастливого кода пути, но это все.

stonemetal
источник
И, аналогично, эти призывы ...throw_ioerror()возвращать ошибки, а не генерировать исключения?
Оромэ
1
@raxacoricofallapatorius Если мы утверждаем, что исключений не существует, то я предполагаю, что программа следует обычной схеме возврата кодов ошибок при сбое и 0 при успехе.
Stonemetal
1
@stonemetal Некоторые языки, такие как Rust и Haskell, используют систему типов для возврата чего-то более значимого, чем код ошибки, без добавления скрытых точек выхода, как это делают исключения. Функция ржавчины, может быть, например, возвращает Result<T, E>перечисление, которое может быть либо Ok<T>, либо Err<E>, с Tтем типа вы хотите , если таковой имеется, и Eбыть типом , представляющий ошибку. Сопоставление с образцом и некоторые конкретные методы упрощают обработку как успехов, так и неудач. Короче говоря, не предполагайте, что отсутствие исключений автоматически означает коды ошибок.
8bittree
1

В дополнение к ответу Чарли:

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

Даже если вы оставите в стороне аргумент о недопустимом состоянии объекта, они всегда вызывают огромную боль при работе с большим приложением.

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

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

konmik
источник
Ну, на самом деле, эти исключения часто могут быть обработаны только в одном месте. Например, в функции «загрузить данные обновления», если происходит сбой SSL, или DNS не разрешен, или веб-сервер возвращает 404, это не имеет значения, перехватить его вверху и сообщить об ошибке пользователю.
Zan Lynx