Блог пользователя removed1

Автор removed1, 15 лет назад, По-русски

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

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

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

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

  • Проголосовать: нравится
  • 0
  • Проголосовать: не нравится

15 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Что за "вкусности" такие, которым мешают исключения? Непонятно )


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


Насчёт памяти непонятно - это где так? В Windows память действительно выделяется, а не просто выдаётся некий абстрактный указатель "от балды" (другое дело, что выделяется она в виртуальном адресном пространстве, однако если память в нём выделить неоткуда, то ошибка произойдёт сразу). При ошибке при выделении памяти вместо корректного указателя вернётся NULL, и код на высокоуровневом языке в ответ на это может сгенерировать исключение.


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

  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    В добавление скажу, что исключения это отличный механизм для сигнализации ошибки и передачи управления обработчику, который неизвестен (неопределен) в текущем контексте.
  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Например, участок программы, который работает с файлами

    {

     openfile("source")

    var data := readfile

    close

    do_something(data)

    openfile("destination")

    write(data)

    closefile

    }


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

15 лет назад, # |
  Проголосовать: нравится +12 Проголосовать: не нравится
Ещё на Java выбросить исключение и перехватить его вызывающим методом - вроде самый быстрый способ выйти из рекурсии и таким образом просигнализировать, что например мы нашли решение во время рекурсивного перебора.
  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Быстрый, в том плане, что пишется быстро, а работает медленно?

    Напротив, на asm'е легко написать так, что возврат из глубокой рекурсии будет моментальным.

15 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

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

Но в олимпиадных они (имхо) бесполезны, ведь там ошибок (исключительных ситуаций) возникать не может.

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

Впервые слышу. И не верю.

  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    Автор имеет ввиду, что вместо того, чтобы использовать глобальный флаг при рекурсивном переборе для сигнализации его окончания, можно выкинуть исключение и поймать его в коде, обрамляющем корень перебора. Бывает довольно удобно, хотя и не является типичным использованием исключений. Категорически не рекомендуется к использованию в неолимпиадном коде.
  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    >Впервые слышу. И не верю. 

    Правильно делаешь. Например, в win выделение памяти (допустим, с помощью VirtualAlloc), не завершится успешно, если в адресном пространстве процесса не осталось достаточного по размеру незарезервированого региона.
15 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

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

Что касается нескольких последних проектов на C# и Java, то там теперь для определения сбоев приходится использовать несколько исключений. Например, функции работы с классами чаще всего возвращают исключение типа FileNotFoundException вместо того, чтобы вернуть неверный код ошибки. Я бы сказал, что это дело вкуса, что именно использовать, однако поскольку ядро .NET написано так, что базируется на исключениях, а не на возврате кодов ошибок, то приходится подстраиваться под это. Проще подстроиться, чем разворотить всю архитектуру и начать с нуля.

15 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится
На олимпиадке надо быть осторожным с исключениями. Мягко говоря сделать одно исключение, которое один раз выйдет из рекурсии - это хороший способ сэкономить время на кодяченье.
А вот если у вас метр входных данных, где каждая строка - либо число, либо дефис, и вы вызываете Integer.parseInt/int.ParseInt с перехваткой исключения чтобы понять, что это дефис - это хороший способ поймать TLE на тесте с миллионом дефисов :о) В принципе нескольких тысяч дефисов хватит чтобы положить решение, потому что выбрасывание исключения работает оооочень медленно. Что в принципе не должно быть критично - ведь исключение предназначено для исключительных ситуаций, когда 0.01 секунды не решает :о) И прежде чем использовать их не по назначению надо понимать и принимать подобный риск.
  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    Казалось бы, в Java обработка исключений должна быть весьма быстрой -- ведь не надо разворачивать стек и вызывать деструкторы, ибо сборка мусора. Это не так?
    • 15 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится
      Я не очень хорошо знаю как это все реализовано, пример про дефисы взят из практики - я так считывал данные и ловил TLE, а потом когда начал разбираться, оно секунду считывало что-то типа 1К или 10К строк. Сделал проверку на дефис до parseInt и все залетало.
    • 15 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Я тоже джаву знаю поверхностно, но кажется, что стек всё равно надо рускручивать (подниматься из рекурсии), и надо уменьшать счётчики использования объектов. Сборка мусора ведь без счётчика ссылок невозможна, насколько я понимаю.


      Но, в принципе, действительно странно - в C++ ведь обработка исключений использует механизмы ОС, которые весьма медленны. В Java наверняка используется собственный подход, который по логике должен быть побыстрей...

      • 15 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится
        Я джаву почти не знаю, но вот в .net никаких счетчиков ссылок нет, например. Сборщик мусора в некоторый момент приостанавливает все нити приложения и начиная с неких корневых точек начинает обходить граф зависимых объектов. Все до чего не добрался считается неиспользуемым. Со счетчиками ссылок есть проблема. Например когда объект A ссылается на B, а тот в свою чередь на A. Таким образом, если бы использовались только счетчики ссылок, то эта парочка никогда бы не уничтожилась.

        Раскручивать стек придется, просто потому что исключение может ловиться на каждом уровне стека вызовов. Более того, отлавливаются исключения исходя из некоторых правил, которые учитывают наследование типов исключений. Это основная причина небыстрой работы их отлова. То есть кидается исключение довольно быстро, а вот ловится долго.
        • 15 лет назад, # ^ |
            Проголосовать: нравится 0 Проголосовать: не нравится
          А, действительно, ссылки же палятся в этом случае :) Хотя вроде с ними тоже можно обходить эти подводные камни?..
          • 15 лет назад, # ^ |
              Проголосовать: нравится +1 Проголосовать: не нравится
            Вообще, описанная выше технология сборки мусора разумеется очень поверхностная. Если бы это было правдой, то наличие листа на 100 миллионов элементов бы вешало все приложение на секунду как минимум при сборке мусора, однако этого не происходит ни в Java ни в .NET.
            В .NET для этого используются поколения. В Java, насколько мне известно, реализация сборки мусора разная в разных релизах, но скорее всего поколения используются тоже.
            Общая идея поколений: каждый объект может быть в одном из трех поколений: нулевом (свежачок), первом и втором. Когда объект создается, он попадает в нулевое поколение. Когда памяти начинает не хватать и инициируется сборка мусора, сборщик обходит только объекты из нулевого поколения, и все объекты, пережившие эту сборку, попадают в первое поколение. Чистка первого поколения происходит только если чистка нулевого не освободила достаточно памяти, при этом все выжившие объекты уходят во второе поколение, чистка второго происходит только если чистка первого не спасла, но выжившие объекты остаются во втором поколении.

            Это тоже в принципе достаточно поверхностно. Но это максимум из того, что я видел в книжках (полагаю я это видел в Рихтере).

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

            Вообще я бы посоветовал почитать про то, как все это реализовано - это достаточно интересно. Например, не все знают, что выделение памяти в управляемых языках намного быстрее, чем в неуправляемых за счет того, что все объекты всегда хранятся подряд без дыр, и выделение памяти просто заключается в сдвиге указателя на конец уже занятой памяти.
            • 15 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится
              Сборка мусора в каком-то конкретном поколении - непростая задача. С одной стороны весь граф обходить не хочется, с другой ведь могут быть объекты второго поколения, которые ссылаются на объекты нулевого, поэтому просто игнорировать объекты старших поколений при обходе нельзя. Все это обходится при помощи страшной штуки "write barrier" :)

              Александр, если найдете какую-нибудь ссылку про счечик ссылок в дотнетном GC, киньте сюда, интересно будет почитать. Я об этом никогда ничего не слышал, и почему-то сомневаюсь, что это так.
            • 15 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится
              Счетчики ссылок используются в Perl и Python, в дополнение к которым -- если если объекты, для которых возможны циклические ссылки -- периодически запускается сканирующий сборщик мусора.

              >чем в неуправляемых за счет того, что все объекты всегда хранятся подряд без дыр, и 
              Следует читать: потому что аллокатор при выделении памяти не ищет свободное место в хипе, а использует новое свободное место -- обычный space-time tradeoff. Кстати, в дотнете объекты больше порогового размера не перемещаются, так что дыры есть и там.


15 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится
Да, по сабжу

> Может быть, я просто не видел области применения, где исключения действительно делали жизнь проще, но где же польза от исключений

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