В большинстве современных языков высокого уровня предусмотрен механизм исключений.
В машинном коде процессоров x86 и многих других существует механизм, также называемый исключениями. Через него осуществляется много полезных вещей, таких как виртуальная память, отображаемые на память файлы, эмуляция не поддерживаемых аппаратно инструкций (использовалось на VAX, где решено было сократить систему команд процессора и эмулировать редко используемые команды, использовалось на x86 без FPU, используется в NTVDM), отладочные функции и т.д. Если среда и язык не поддерживают такие механизмы, то реализация кода, используещего ту же функциональность, была бы в значительно длинее.
В отличие от процессорных исключений, в языковых исключениях управление передается "наверх" до внешнего блока, который "поймает" исключение (в ЛИСПе более богатая модель, но в большинстве языков этого нет). При этом контекст, в котором произошла ошибка, будет уничтожен. Что уже делает этот механизм непригодным для реализации ряда "вкусностей". Есть и другие проблемы. Например, отловить момент, когда не хватает памяти, по таким исключениям невозможно -- в средах с виртуальной памятью выделение памяти всегда завершается успешно, а ошибка возникает при попытке обращения к ней.
Может быть, я просто не видел области применения, где исключения действительно делали жизнь проще, но где же польза от исключений?
Что за "вкусности" такие, которым мешают исключения? Непонятно )
То,что все контексты до первого выполнившегося обработчика уничтожаются - как раз позволяет писать стабильный код, который, например, будет легко и прозрачно обращаться с ресурсами.
Насчёт памяти непонятно - это где так? В Windows память действительно выделяется, а не просто выдаётся некий абстрактный указатель "от балды" (другое дело, что выделяется она в виртуальном адресном пространстве, однако если память в нём выделить неоткуда, то ошибка произойдёт сразу). При ошибке при выделении памяти вместо корректного указателя вернётся NULL, и код на высокоуровневом языке в ответ на это может сгенерировать исключение.
Казалось бы, совершенно очевидно, что исключения дают возможность писать устойчивые программы, корректно реагирующие на различные возможные ошибки, и при этом затрачивать минимум усилий по сравнению с "традиционным" подходом. Конечно, насчёт лёгкости можно поспорить - по крайней мере, надо знать в своём языке всякие подводные камни и обходить их. Есть также некоторое падение производительности, но при разумном использовании исключений (именно как средство обработки ошибок, а не как элемент нормальной логики программы) этот момент перевешивается всеми плюсами.
Например, участок программы, который работает с файлами
{
openfile("source")
var data := readfile
close
do_something(data)
openfile("destination")
write(data)
closefile
}
Запись завершилась неудачно (например нет места на диске или пользователь выдернул сменный носитель), а когда исполнение программы поднимется на уровень выше до обработчика catch, стек будет уже отмотан и данные потеряны, и предложить пользователю сохранить данные в другом месте мы не сможем.
Быстрый, в том плане, что пишется быстро, а работает медленно?
Напротив, на asm'е легко написать так, что возврат из глубокой рекурсии будет моментальным.
В большинстве реальных (не олимпиадных) программ каждая вторая строка - вызов какой-нибудь функции/метода, каждый из которых может вернуть код ошибки, и надежный код должен их все проверять. В этих условиях механизм исключений позволяет почти полностью забыть об обработке ошибок, сосредоточиться на основной линии алгоритма, т.е. облегчает написание надежного кода, улучшает читаемость исходника.
Но в олимпиадных они (имхо) бесполезны, ведь там ошибок (исключительных ситуаций) возникать не может.
в средах с виртуальной памятью выделение памяти всегда завершается успешно, а ошибка возникает при попытке обращения к ней.
Впервые слышу. И не верю.
Я бы сказал, что исключения в большинстве современных языков стали выполнять роль механизма возврата значения при ошибке - во всяком случае, в моем опыте это именно так. Если в былые времена в C++ я проверял возвратные значения большинства функций (особенно работающих с файлами и прочими ресурсами), и в случае возврата неверного значения пытался определить, что именно произошло, и вывести какое-то адекватное сообщение в лог или на экран пользователю.
Что касается нескольких последних проектов на C# и Java, то там теперь для определения сбоев приходится использовать несколько исключений. Например, функции работы с классами чаще всего возвращают исключение типа FileNotFoundException вместо того, чтобы вернуть неверный код ошибки. Я бы сказал, что это дело вкуса, что именно использовать, однако поскольку ядро .NET написано так, что базируется на исключениях, а не на возврате кодов ошибок, то приходится подстраиваться под это. Проще подстроиться, чем разворотить всю архитектуру и начать с нуля.
А вот если у вас метр входных данных, где каждая строка - либо число, либо дефис, и вы вызываете Integer.parseInt/int.ParseInt с перехваткой исключения чтобы понять, что это дефис - это хороший способ поймать TLE на тесте с миллионом дефисов :о) В принципе нескольких тысяч дефисов хватит чтобы положить решение, потому что выбрасывание исключения работает оооочень медленно. Что в принципе не должно быть критично - ведь исключение предназначено для исключительных ситуаций, когда 0.01 секунды не решает :о) И прежде чем использовать их не по назначению надо понимать и принимать подобный риск.
Я тоже джаву знаю поверхностно, но кажется, что стек всё равно надо рускручивать (подниматься из рекурсии), и надо уменьшать счётчики использования объектов. Сборка мусора ведь без счётчика ссылок невозможна, насколько я понимаю.
Но, в принципе, действительно странно - в C++ ведь обработка исключений использует механизмы ОС, которые весьма медленны. В Java наверняка используется собственный подход, который по логике должен быть побыстрей...
Раскручивать стек придется, просто потому что исключение может ловиться на каждом уровне стека вызовов. Более того, отлавливаются исключения исходя из некоторых правил, которые учитывают наследование типов исключений. Это основная причина небыстрой работы их отлова. То есть кидается исключение довольно быстро, а вот ловится долго.
В .NET для этого используются поколения. В Java, насколько мне известно, реализация сборки мусора разная в разных релизах, но скорее всего поколения используются тоже.
Общая идея поколений: каждый объект может быть в одном из трех поколений: нулевом (свежачок), первом и втором. Когда объект создается, он попадает в нулевое поколение. Когда памяти начинает не хватать и инициируется сборка мусора, сборщик обходит только объекты из нулевого поколения, и все объекты, пережившие эту сборку, попадают в первое поколение. Чистка первого поколения происходит только если чистка нулевого не освободила достаточно памяти, при этом все выжившие объекты уходят во второе поколение, чистка второго происходит только если чистка первого не спасла, но выжившие объекты остаются во втором поколении.
Это тоже в принципе достаточно поверхностно. Но это максимум из того, что я видел в книжках (полагаю я это видел в Рихтере).
Поведение сборщика в разных поколениях немного отличается. Так, я слышал, что в каком-то поколении счетчики ссылок активно используются.
Вообще я бы посоветовал почитать про то, как все это реализовано - это достаточно интересно. Например, не все знают, что выделение памяти в управляемых языках намного быстрее, чем в неуправляемых за счет того, что все объекты всегда хранятся подряд без дыр, и выделение памяти просто заключается в сдвиге указателя на конец уже занятой памяти.
Александр, если найдете какую-нибудь ссылку про счечик ссылок в дотнетном GC, киньте сюда, интересно будет почитать. Я об этом никогда ничего не слышал, и почему-то сомневаюсь, что это так.
> Может быть, я просто не видел области применения, где исключения действительно делали жизнь проще, но где же польза от исключений
Найди любой мануал по тому, как на C++ написать клиент-серверное приложение, и посмотри на код установки соединения с любой стороны. Там выполняется четыре действия, которые занимают 20 строк кода, потому что после каждого действия надо проверить код ошибки и отреагировать на это. Кстати, реакция чаще всего при этом является заглушкой, потому что автор мануала обычно не имеет никакого представления, что же конечный читатель хочет делать, если подключиться не удалось. Казалось бы, если это устраивает, то да, исключения Вам не нужны :о) А если хочется выполняя четыре действия писать четыре строки кода, и при этом там, где вы не знаете, как реагировать на ошибку, не реагировать на нее, то исключения все-таки кажутся полезным инструментом.