Обработка ошибок при работе с файлами с
Не генерируйте исключения повторно
Я натыкаюсь на это снова и снова. Люди оказываются сбиты с толку тем, что исходный стек трейс «волшебным образом» исчезает при обработке ошибок. Чаще всего это вызвано повторной генерацией исключений. Давайте посмотрим на пример, в котором у нас есть вложенные try/catch :
Как вы, наверное, уже догадались, внутренний try/catch перехватывает, регистрирует и проглатывает исключение. Чтобы пробросить SpecificException в глобальный блок catch для его обработки, вам нужно пробросить его в стек. Вы можете сделать следующее:
Основное отличие здесь состоит в том, что в первом примере повторно генерируется SpecificException , что приводит к сбросу стек трейса исходного исключения, в то время как второй пример сохраняют все детали исходного исключения. Почти всегда предпочтительнее использовать второй пример.
Декорируйте исключения
Я достаточно редко вижу реализацию этой рекомендации на практике. Все исключения расширяют Exception, в котором есть словарь Data. Словарь можно использовать для включения дополнительной информации об ошибке. Отображается ли эта информация в вашем логе, зависит от того, какой фреймворк логирования и хранилище вы используете. В elmah.io записи Data отображаются на вкладке Data.
Информацию в словарь Data вносится посредством добавьте пар ключ/значение:
В этом примере я добавляю ключ с именем user с потенциальным именем пользователя, хранящимся в потоке.
Вы также можете декорировать исключения, сгенерированные сторонним кодом. Добавьте try/catch :
Код перехватывает любые исключения, генерируемые методом SomeCall , и добавляет в них имя пользователя. Посредством добавления ключевого слова throw в блок catch исходное исключение пробрасывается дальше по стеку.
Перехватывайте в первую очередь наиболее специфические исключения
Вероятнее всего, у вас есть где-то код, похожий на этот:
В следующем примере я четко демонстрирую понимание, какие исключения следует ожидать и как поступать с каждым конкретным типом:
Обратите внимание, что, хотя приведенный выше код служит для объяснения порядка обработки исключений, реализация потока управления, используя исключения подобным образом — практика не очень хорошая. Это прекрасная подводка к следующему совету:
Старайтесь избегать исключений
Может показаться очевидным, что нужно избегать исключений. Но многих методов, генерирующих исключение, можно избежать с помощью защитного программирования.
Одно из самых распространенных исключений — NullReferenceException . В некоторых случаях вы можете разрешить null , но забыть проверить на null . Вот пример, который генерирует NullReferenceException :
Доступ к a выбрасывает исключение. Хорошо, но представьте, что a предоставляется в качестве параметра.
Если вы хотите разрешить city с нулевым значением, вы можете избежать исключения, используя null-condition оператор:
Другой распространенный пример исключений — это анализ чисел или логических значений. В следующем примере будет сгенерировано FormatException :
Строка invalid не может быть распаршена в виде целого числа. Чтобы не оборачивать это в try/catch , int предоставляет интересный метод, который вы, вероятно, уже использовали 1000 раз:
В случае, если invalid может быть распаршена как int , TryParse возвращает true и помещает распаршенное значение в переменную i . Еще одно исключение удалось избежать.
Создавайте пользовательские исключения
Класс MyVerySpecializedException (возможно, это не то имя класса, которое вы должны использовать в качестве примера :D) реализует три конструктора, которые должен иметь каждый класс исключения. Кроме того, я добавил свойство Status в качестве примера дополнительных данных. Это позволит нам написать такой код:
Используя ключевое слово when , я могу перехватить MyVerySpecializedException , когда значение свойства Status равно 500. Все остальные сценарии попадут в общий catch MyVerySpecializedException .
Логируйте исключения
Это кажется таким очевидным. Но я видел слишком много ошибок в коде в следующих строках при использовании этого шаблона:
Логирование как неперехваченных, так и перехваченных исключений — это меньшее, что вы можете сделать для своих пользователей. Нет ничего хуже, чем когда пользователи обращаются в вашу службу поддержки, и вы даже не подозреваете, какие были ошибки и что произошло. В этом вам поможет ведение логов.
Исключения и утверждения
Чтение элементов из файла (последовательный доступ)
Макрос assert()
Макрос, проверяющий условие expression (его результат должен быть числом) во время выполнения. Если условие не выполняется ( expression равно нулю), он печатает в stderr значения __FILE__ , __LINE__ , __func__ и expression в виде строки, после чего вызывает функцию abort() .
Если макрос NDEBUG определён перед включением , то assert() разворачивается в ((void) 0) и не делает ничего. Используется в отладочных целях.
Спецификации исключений и noexcept
Спецификации исключений были введены в C++ как способ указания исключений, которые может вызывать функция. Однако спецификации исключений выдают проблемы на практике и являются устаревшими в стандарте "черновик C++ 11". Мы рекомендуем не использовать throw спецификации исключений, кроме throw() , что указывает, что функция не допускает исключений для экранирования. если необходимо использовать спецификации исключений устаревшей формы throw( type-name ) , MSVC поддержка ограничена. Дополнительные сведения см. в разделе спецификации исключений (throw). noexcept Описатель вводится в c++ 11 в качестве предпочтительного варианта throw() .
Функции setjmp() и longjmp()
Вот мы и подошли к самому интересному – функциям нелокальных переходов. setjmp() и longjmp() работают по принципу goto, но в отличие от него позволяют перепрыгивать из одного места в другое в пределах всей программы, а не одной функции.
int setjmp(jmp_buf env);
Сохраняет информацию о контексте выполнения программы (регистры микропроцессора и прочее) в env . Возвращает 0 , если была вызвана напрямую или value , если из longjmp() .
void longjmp(jmp_buf env, int value);
Восстанавливает контекст выполнения программы из env , возвращает управление setjmp() и передаёт ей value .
Используя setjmp() и longjmp () можно реализовать механизм исключений. Во многих языках высокого уровня (например, в Perl) исключения реализованы через них.
Внимание! Функции setjmp() и longjmp () в первую очередь применяются в системном программировании, и их использование в клиентском коде не рекомендуется. Их применение ухудшает читаемость программы и может привести к непредсказуемым ошибкам. Например, что произойдёт, если вы прыгните не вверх по стеку – в вызывающую функцию, а в параллельную, уже завершившую выполнение?
Класс FileStream представляет возможности по считыванию из файла и записи в файл. Он позволяет работать как с текстовыми файлами, так и с бинарными. Он создаётся следующим образом: Здесь в конструктор передается два параметра: путь к файлу и перечисление FileMode. Данное перечисление указывает на режим доступа к файлу и может принимать следующие значения:
- Append: если файл существует, то текст добавляется в конец файл. Если файла нет, то он создается. Файл открывается только для записи.
- Create: создается новый файл. Если такой файл уже существует, то он перезаписывается
- CreateNew: создается новый файл. Если такой файл уже существует, то он приложение выбрасывает ошибку
- Open: открывает файл. Если файл не существует, выбрасывается исключение
- OpenOrCreate: если файл существует, он открывается, если нет - создается новый
- Truncate: если файл существует, то он перезаписывается. Файл открывается только для записи.
Также можно использовать статические методы класса File, например
Для работы с бинарными файлами предназначена пара классов BinaryWriter и BinaryReader. Эти классы позволяют читать и записывать данные в двоичном формате.
Обработка исключений при операциях ввода-вывода
По причине зависимости от операционной системы иногда идентичные условия (например, отсутствие указанного каталога) могут создавать в методах ввода-вывода любое исключение из класса ввода-вывода. Это означает, что при вызове интерфейсов API ввода-вывода ваш код должн быть готов обработать все такие исключения или большую их часть, как показано в следующей таблице:
Функции работы с errno
Получив код ошибки, хочется сразу получить по нему её описание. К счастью, ISO C предлагает целый набор полезных функций.
void perror(const char *s);
strerror() не безопасная функция. Во-первых, возвращаемая ею строка не является константной. При этом она может храниться в статической или в динамической памяти в зависимости от реализации. В первом случае её изменение приведёт к ошибке времени выполнения. Во-вторых, если вы решите сохранить указатель на строку, и после вызовите функцию с новым кодом, все прежние указатели будут указывать уже на новую строку, ибо она использует один буфер для всех строк. В-третьих, её поведение в многопоточной среде не определено в стандарте. Впрочем, в QNX она объявлена как thread safe.
Поэтому в новом стандарте ISO C11 были предложены две очень полезные функции.
size_t strerrorlen_s(errno_t errnum);
Возвращает длину строки с описанием ошибки errnum .
errno_t strerror_s(char *buf, rsize_t buflen, errno_t errnum);
Копирует строку с описание ошибки errnum в буфер buf длиной buflen .
Функции входят в Annex K (Bounds-checking interfaces), вызвавший много споров. Он не обязателен к выполнению и целиком не реализован ни в одной из свободных библиотек. Open Watcom C/C++ (Windows), Slibc (GNU libc) и Safe C Library (POSIX), в последней, к сожалению, именно эти две функции не реализованы. Тем не менее, их можно найти в коммерческих средах разработки и системах реального времени, Embarcadero RAD Studio, INtime RTOS, QNX.
Стандарт POSIX.1-2008 определяет следующие функции:
char *strerror_l(int errnum, locale_t locale);
Возвращает строку, содержащую локализованное описание ошибки errnum , используя locale . Безопасна в многопоточной среде. Не реализована в Mac OS X, FreeBSD, NetBSD, OpenBSD, Solaris и прочих коммерческих UNIX. Реализована в Linux, MINIX 3 и Illumos (OpenSolaris).
int strerror_r(int errnum, char *buf, size_t buflen);
Копирует строку с описание ошибки errnum в буфер buf длиной buflen . Если buflen меньше длины строки, лишнее обрезается. Безопасна в многоготочной среде. Реализована во всех UNIX.
Увы, никакого аналога strerrorlen_s() в POSIX не определили, поэтому длину строки можно выяснить лишь экспериментальным путём. Обычно 300 символов хватает за глаза. GNU C Library в реализации strerror() использует буфер длиной в 1024 символа. Но мало ли, а вдруг?
Чтение элементов из файла (произвольный доступ)
В четырёх следующих задачах используем Метод long Seek(long offset, SeekOrigin origin) устанавливает позицию в потоке со смещением на количество байт, указанных в параметре offset. SeekOrigin - перечисление с тремя значениями
- SeekOrigin.Begin: начало файла
- SeekOrigin.End: конец файла
- SeekOrigin.Current: текущая позиция в файле
Смещение может отрицательным, тогда курсор сдвигается назад, если положительное - то вперед. Если мы хотим прочитать из файла целых чисел третье число, мы пишем:
- Дан бинарный файл целых чисел. Обнулить его минимальный элемент (считать, что в файле он единственный). Если файл пуст, ничего не делать.
Указание. Во-первых, следует найти позицию минимального элемента (пользуясь Position ).
Во вторых, с помощью метода Seek , следует перейти в позицию минимального элемента и записать в файл переменную с нулевым значением.
Тестирование. В основной программе явно создайте и обработайте четыре файла (обязательно проверить пустой и файл из одного элемента). Для каждого файла: распечатать исходное содержимое, обнулить минимальный элемент, распечатать содержимое файла после изменения.
Указание к тестированию. Удобно записать имена созданных файлов в массив строк и обработать их в цикле. - Дан бинарный файл целых чисел. Увеличить все его элементы в два раза.
Тестирование. В основной программе явно создайте и обработайте четыре файла (обязательно проверить пустой и файл из одного элемента). - Дан файл целых чисел (возможно пустой). Инвертировать его (то есть изменить в нём порядок элементов на обратный).
Замечание. Не забудьте, что использовать вспомогательный файл запрещается.
Тестирование. В основной программе создать и обработать четыре файла (обязательно проверить пустой, файл из одного и двух элементов). - Дан бинарный файл. Удвоить его размер, записав в конец файла все его исходные элементы в обратном порядке.
Замечание. Не забудьте, что использовать вспомогательный файл запрещается.
Тестирование. В основной программе явно создайте и обработайте три файла (обязательно пустой файл, файл из одного целого числа).
исключения C++ и Windows исключения SEH
программы C и C++ могут использовать механизм структурированной обработки исключений (SEH) в Windows операционной системе. Понятия SEH похожи на объекты в исключениях C++, за исключением того, что SEH использует __try конструкции, __except и __finally , а не try и catch . в компиляторе Microsoft C++ (MSVC) исключения C++ реализуются для SEH. Однако при написании кода C++ используйте синтаксис исключения C++.
Сопоставление кодов ошибок с исключениями
Например, при вызове метода в операционной системе Windows код ошибки ERROR_FILE_NOT_FOUND (или 0x02) преобразуется в исключение FileNotFoundException, а код ошибки ERROR_PATH_NOT_FOUND (или 0x03) — в DirectoryNotFoundException.
К сожалению, точные условия возникновения определенных кодов ошибок в операционной системе часто не документируются или документируются в недостаточном объеме. Это означает, что возможны непредвиденные исключения. Например, при работе с каталогом логично ожидать, что передача недопустимого пути в конструктор DirectoryInfo приведет к созданию исключения DirectoryNotFoundException. Но в этой ситуации может создаваться и FileNotFoundException.
Основные рекомендации
Надежная обработка ошибок является сложной задачей в любом языке программирования. Хотя исключения предоставляют несколько функций, которые поддерживают хорошую обработку ошибок, они не могут выполнить всю работу. Чтобы реализовать преимущества механизма исключения, помните об исключениях при проектировании кода.
Используйте утверждения, чтобы проверить наличие ошибок, которые не должны возникать. Используйте исключения для проверки ошибок, которые могут возникать, например, ошибок при проверке входных данных для параметров открытых функций. Дополнительные сведения см. в разделе исключения и утверждения .
Используйте исключения, если код, обрабатывающий ошибку, отделен от кода, который обнаруживает ошибку одним или несколькими промежуточными вызовами функций. Рассмотрите возможность использования кодов ошибок в циклах, критических для производительности, когда код, обрабатывающий ошибку, тесно связан с кодом, который его обнаруживает.
Для каждой функции, которая может выдавать или распространять исключение, следует предоставить одно из трех гарантий исключений: строгая гарантия, Базовая гарантия или "Throw" (Except). Дополнительные сведения см. в разделе руководство. проектирование безопасности исключений.
Вызывайте исключения по значению, перехватите их по ссылке. Не перехватывайте объекты, которые не могут быть обработаны.
Не используйте спецификации исключений, которые являются устаревшими в C++ 11. Дополнительные сведения см. в разделе спецификации исключений и noexcept раздел.
Используйте типы исключений стандартной библиотеки при их применении. Наследовать пользовательские типы исключений от exception иерархии классов .
Не разрешать исключения для экранирования из деструкторов или функций освобождения памяти.
Средства System.IO.File для работы с текстовыми файлами
- Дан текстовый файл. Определить количество пустых строк в этом файле, используя статический метод ReadLines класса System.IO.File .
- Дан csv-файл, содержащий целые числа. Найти сумму чисел в каждой строке файла (решение оформить в виде функции, возвращающей массив целых чисел). Для пустой строки следует возвращать ноль.
Замечание 1. Каждая строка файла в формате CSV (comma-separated values — значения, разделённые запятыми) содержит значения, разделённые запятыми.
Указание 1. Использовать метод Split для извлечения чисел из строк и метод int.Parse для преобразования их к типу integer для суммирования.
Замечание 2. Задачу можно решать с использованием явных циклов, либо с использованием методов последовательностей. - Данная задача не требует чтения содержимого файлов.
- Создать в текущем каталоге папку text_files , создать там несколько текстовых файлов просто «руками», можете скопировать туда имеющиеся файлы).
- Вывести имена файлов каталога text_files (процедура с одним параметром — именем каталога).
- Переименовать все текстовые файлы каталога text_files , добавив к именам префикс help- . Это следует выполнить в процедуру с двумя параметрами — именем каталога и префиксом).
- Снова вывести имена файлов каталога.
Указания.
Чтобы переименовать файл, используйте статический метод System.IO.File.Move .
Функции atexit(), exit() и abort()
int atexit(void (*func)(void));
Регистрирует функции, вызываемые при нормальном завершении работы программы в порядке, обратном их регистрации. Можно зарегистрировать до 32 функций.
_Noreturn void exit(int exit_code);
Главное преимущество exit() в том, что она позволяет завершить программу не только из main() , но и из любой вложенной функции. К примеру, если в глубоко вложенной функции выполнилось (или не выполнилось) некоторое условие, после чего дальнейшее выполнение программы теряет всякий смысл. Подобный приём (early exit) широко используется при написании демонов, системных утилит и парсеров. В интерактивных программах с бесконечным главным циклом exit() можно использовать для выхода из программы при выборе нужного пункта меню.
_Noreturn void abort(void);
Вызывает аварийное завершение программы, если сигнал не был перехвачен обработчиком сигналов. Временные файлы не уничтожаются, закрытие потоков определяется реализацией. Самое главное отличие вызовов abort() и exit(EXIT_FAILURE) в том, что первый посылает программе сигнал SIGABRT , его можно перехватить и произвести нужные действия перед завершением программы. Записывается дамп памяти программы (core dump file), если они разрешены. При запуске в отладчике он перехватывает сигнал SIGABRT и останавливает выполнение программы, что очень удобно в отладке.
Вывод в отладчике:
В случае критической ошибки нужно использовать функцию abort() . К примеру, если при выделении памяти или записи файла произошла ошибка. Любые дальнейшие действия могут усугубить ситуацию. Если завершить выполнение обычным способом, при котором производится сброс потоков ввода — вывода, можно потерять ещё неповрежденные данные и временные файлы, поэтому самым лучшим решением будет записать дамп и мгновенно завершить программу.
В случае же некритической ошибки, например, вы не смогли открыть файл, можно безопасно выйти через exit() .
Обработка IOException
IOException является базовым классом для исключений в пространстве имен System.IO и создается для любого кода ошибки, который не имеет сопоставления с определенным типом исключения. Это означает, что оно может появиться в любой операции ввода-вывода.
Так как IOException является базовым классом для других типов исключений в пространстве имен System.IO, его нужно обрабатывать в блоке catch после обработки других исключений, связанных с вводом-выводом.
Обратите внимание, что в коде обработки исключений IOException всегда нужно обрабатывать последним. Иначе блоки catch для производных классов не проверяются, ведь это исключение является базовым классом для всех остальных.
В случае IOException можно получить дополнительные сведения об ошибке из свойства IOException . Чтобы преобразовать значение HResult в код ошибки Win32, отбросьте верхние 16 бит из 32-разрядного значения. В приведенной ниже таблице перечислены коды ошибок, которые могут быть заключены в IOException.
HResult | Константа | Описание |
---|---|---|
ERROR_SHARING_VIOLATION | 32 | Отсутствует имя файла, или файл или каталог уже используется. |
ERROR_FILE_EXISTS | 80 | Файл уже существует. |
ERROR_INVALID_PARAMETER | 87 | Методу передан недопустимый аргумент. |
ERROR_ALREADY_EXISTS | 183 | Файл или каталог уже существует. |
Для обработки этих исключений можно применить предложение When в инструкции catch, как показано в приведенном ниже примере.
В современных C++ в большинстве случаев предпочтительным способом сообщить и обрабатывались как логические ошибки, так и ошибки времени выполнения — использовать исключения. Особенно это касается того, что стек может содержать несколько вызовов функций между функцией, которая обнаруживает ошибку, и функцией, которая имеет контекст для ее устранения. Исключения предоставляют формальный, четко определенный способ для кода, который обнаруживает ошибки для передачи информации вверх по стеку вызовов.
Переменная errno и коды ошибок
errno – переменная, хранящая целочисленный код последней ошибки. В каждом потоке существует своя локальная версия errno, чем и обусловливается её безопасность в многопоточной среде. Обычно errno реализуется в виде макроса, разворачивающегося в вызов функции, возвращающей указатель на целочисленный буфер. При запуске программы значение errno равно нулю.
Стандарт ISO C определяет следующие коды:
- EDOM – (Error domain) ошибка области определения.
- EILSEQ – (Error invalid sequence) ошибочная последовательность байтов.
- ERANGE – (Error range) результат слишком велик.
Нехитрый скрипт печатает в консоль коды ошибок, их символические имена и описания:
Если вызов функции завершился ошибкой, то она устанавливает переменную errno в ненулевое значение. Если же вызов прошёл успешно, функция обычно не проверяет и не меняет переменную errno. Поэтому перед вызовом функции её нужно установить в 0 .
Как видите, описания ошибок в спецификации функции iconv() более информативны, чем в .
Исключения и производительность
Механизм исключения имеет минимальные затраты на производительность, если исключение не создается. При возникновении исключения стоимость прохода стека и его очистки приблизительно сравнима с затратами на вызов функции. Дополнительные структуры данных необходимы для контроля стека вызовов после того, как try был выполнен блок, и при возникновении исключения требуются дополнительные инструкции для очистки стека. Однако в большинстве случаев затраты на производительность и объем памяти не являются существенными. Негативное воздействие исключений на производительность может быть значительным только для систем с ограниченным объемом памяти. Кроме того, в циклах, критических с точки зрения производительности, часто возникает ошибка, и существует тесная связь между кодом и его обработкой. В любом случае невозможно понять фактическую стоимость исключений без профилирования и измерения. Даже в редких случаях, когда стоимость существенна, можно взвесить ее на более высокую правильность, упростить обслуживание и другие преимущества, предоставляемые хорошо спроектированной политикой исключений.
Использовать исключения для кода исключительного пользования
Ошибки программы часто делятся на две категории: логические ошибки, вызванные ошибками программирования, например, ошибкой «индекс вне диапазона». И ошибки времени выполнения, которые выходят за рамки управления программистом, например "ошибка" Сетевая служба недоступна ". В программировании в стиле C и в COM Управление отчетами об ошибках осуществляется либо путем возвращения значения, представляющего код ошибки, либо кода состояния для конкретной функции, либо путем установки глобальной переменной, которую вызывающий может дополнительно получить после каждого вызова функции, чтобы проверить, были ли обнаружены ошибки. Например, при программировании COM для передачи ошибок вызывающему объекту используется возвращаемое значение HRESULT. API-интерфейс Win32 содержит GetLastError функцию для получения последней ошибки, о которой сообщил стек вызовов. В обоих случаях для распознавания кода и реагирования на него требуется вызывающая сторона. Если вызывающий объект не обрабатывает код ошибки явным образом, программа может аварийно завершить работу без предупреждения. Или можно продолжить выполнение с использованием неверных данных и получить неверные результаты.
Исключения являются предпочтительными в современных C++ по следующим причинам:
Исключение приводит к тому, что вызывающий код распознает состояние ошибки и обрабатывает его. Необработанные исключения останавливают выполнение программы.
Механизм обратной записи исключений уничтожает все объекты в области действия после возникновения исключения в соответствии с четко определенными правилами.
Исключение позволяет четко отделить код, который определяет ошибку, и код, обрабатывающий ошибку.
В следующем упрощенном примере показан синтаксис, необходимый для генерации и перехвата исключений в C++.
Основные метода класса BinaryWriter
- Close(): закрывает поток и освобождает ресурсы
- Flush(): очищает буфер, дописывая из него оставшиеся данные в файл
- Seek(): устанавливает позицию в потоке
- Write(): записывает данные в поток
Объект BinaryWriter создаётся так:
Для того, чтобы закрыть потоки и освободить ресурсы можно использовать метод Close(). Но удобнее использовать директиву using или можно сократить запись:
Бинарные файлы (запись)
- Создайте метод который записывает в файл целых чисел набор значений, перечисленных через запятую.
Указание Есть разные виды ошибок, при котором работа с файлом невозможна, например отсутствие прав доступа к файлу. Чтобы программа не "вылетала", используйте конструкцию try. catch Используйте try. catch в методе или поместите в блок try вызов метода.
Обработка текстовых файлов
Читайте также: