Обработка исключений ввода-вывода
Исключения ввода-вывода происходят, когда что-то пошло не так при взаимодействии с внешним миром в действии ввода-вывода, являющемся частью функции main. Например, мы пытаемся открыть файл, и тут оказывается, что он был удалён, или ещё что-нибудь в этом духе. Посмотрите на программу, открывающую файл, имя которого передаётся в командной строке, и говорящую нам, сколько строк содержится в файле:
import System.Environment
import System.IO
main = do
(fileName:_) <– getArgs
contents <– readFile fileName
putStrLn $ "В этом файле " ++ show (length (lines contents)) ++
" строк!"
Очень простая программа. Мы выполняем действие ввода-вывода getArgs и связываем первую строку в возвращённом списке с идентификатором fileName. Затем связываем имя contents с содержимым файла. Применяем функцию lines к contents, чтобы получить список строк, считаем их количество и передаём его функции show, чтобы получить строковое представление числа. Это работает – но что получится, если передать программе имя несуществующего файла?
$ ./linecount dont_exist.txt
linecount: dont_exist.txt: openFile: does not exist (No such file or directory)
Ага, получили ошибку от GHC с сообщением, что файла не существует! Наша программа «упала». Но лучше бы она печатала красивое сообщение, если файл не найден. Как этого добиться? Можно проверять существование файла, прежде чем попытаться его открыть, используя функцию doesFileExist из модуля System.Directory.
import System.Environment
import System.IO
import System.Directory
main = do
(fileName:_) <– getArgs
fileExists <– doesFileExist fileName
if fileExists
then do
contents <– readFile fileName
putStrLn $ "В этом файле " ++
show (length (lines contents)) ++
" строк!"
else putStrLn "Файл не существует!"
Мы делаем вызов fileExists <– doesFileExist fileName, потому что функция doesFileExist имеет тип doesFileExist :: FilePath –> IO Bool; это означает, что она возвращает действие ввода-вывода, содержащее булевское значение, которое говорит нам, существует ли файл. Мы не можем напрямую использовать функцию doesFileExist в условном выражении.
Другим решением было бы использовать исключения. В этом контексте они совершенно уместны. Ошибка при отсутствии файла происходит в момент выполнения действия ввода-вывода, так что его перехват в секции ввода-вывода лёгок и приятен. К тому же, обработка исключений позволяет сделать этот код менее громоздким:
import Prelude hiding (catch)
import Control.Exception
import System.Environment
countLines :: String -> IO ()
countLines fileName = do
contents <- readFile fileName
putStrLn $ "В этом файле " ++ show (length (lines contents)) ++
" строк!"
handler :: IOException -> IO ()
handler e = putStrLn "У нас проблемы!"
main = do
(fileName:_) <- getArgs
countLines fileName `catch` handler
Здесь мы определяем обработчик handler для всех исключений ввода-вывода и пользуемся функцией catch для перехвата исключения, возникающего в функции countLines.
Попробуем:
$ ./linecount linecount.hs
В этом файле 17 строк!
$ ./linecount dont_exist.txt
У нас проблемы!
Исключение ввода-вывода может быть вызвано целым рядом причин, среди которых, помимо отсутствия файла, может быть также отсутствие права на чтение файла или вообще отказ жёсткого диска. В обработчике мы не проверяли, какой вид исключения IOException получили. Мы просто возвращаем строку "У нас проблемы", что бы ни произошло.
Простой перехват всех типов исключений в одном обработчике – плохая практика в языке Haskell, так же как и в большинстве других языков. Что если произошло какое-либо другое исключение, которое мы не хотели бы перехватывать, например прерывание программы? Вот почему мы будем делать то же, что делается в других языках: проверять, какой вид исключения произошёл. Если это тот вид, который мы ожидали перехватить, вызовем обработчик. Если это нечто другое, мы не мешаем исключению распространяться далее. Давайте изменим нашу программу так, чтобы она перехватывала только исключение, вызываемое отсутствием файла:
import Prelude hiding (catch)
import Control.Exception
import System.Environment
import System.IO.Error (isDoesNotExistError)
countLines :: String -> IO () countLines fileName = do
contents <- readFile fileName
putStrLn $ "В этом файле " ++ show (length (lines contents)) ++
" строк!"
handler :: IOException -> IO ()
handler e
| isDoesNotExistError e = putStrLn "Файл не существует!"
| otherwise = ioError e
main = do
(fileName:_) <- getArgs
countLines fileName `catch` handler
Программа осталась той же самой, но поменялся обработчик, который мы изменили таким образом, что он реагирует только на одну группу исключений ввода-вывода. С этой целью мы воспользовались предикатом isDoesNotExistError из модуля System.IO.Error. Мы применяем его к исключению, переданному в обработчик, чтобы определить, было ли исключение вызвано отсутствием файла. В данном случае мы используем охранные выражения, но могли бы использовать и условное выражение if–then–else. Если исключение вызвано другими причинами, перевызываем исключение с помощью функции ioError.
ПРИМЕЧАНИЕ. Функции try, catch, ioError и некоторые другие объявлены одновременно в модулях System.IO.Error (устаревший вариант) и Control.Exception (современный вариант), поэтому подключение обоих модулей (например, для использования предикатов исключений ввода-вывода) требует скрывающего или квалифицированного импорта либо же, как в предыдущем примере, явного указания импортируемых функций.
Итак, исключение, произошедшее в действии ввода-вывода countLines, но не по причине отсутствия файла, будет перехвачено и перевызвано в обработчике:
$ ./linecount dont_exist.txt
Файл не существует!
$ ./linecount norights.txt
linecount: noaccess.txt: openFile: permission denied (Permission denied)
Существует несколько предикатов, предназначенных для определения вида исключения ввода-вывода:
• isAlreadyExistsError (файл уже существует);
• isDoesNotExistError (файл не существует);
• isAlreadyInUseError (файл уже используется);
• isFullError (не хватает места на диске);
• isEOFError (достигнут конец файла);
• isIllegalOperation (выполнена недопустимая операция);
• isPermissionError (недостаточно прав доступа).
Пользуясь этими предикатами, можно написать примерно такой обработчик:
handler :: IOException -> IO ()
handler e
| isDoesNotExistError e = putStrLn "Файл не существует!"
| isPermissionError e = putStrLn "Не хватает прав доступа!"
| isFullError e = putStrLn "Освободите место на диске!"
| isIllegalOperation e = putStrLn "Караул! Спасите!"
| otherwise = ioError e
Убедитесь, что вы перевызываете исключение, если оно не подходит под ваши критерии; в противном случае ваша программа иногда будет «падать» молча, что крайне нежелательно.
Модуль System.IO.Error также экспортирует функции, которые позволяют нам получать атрибуты исключения, например дескриптор файла, вызвавшего исключение, или имя файла. Все эти функции начинаются с префикса ioe; их полный список вы можете найти в документации. Скажем, мы хотим напечатать имя файла в сообщении об ошибке. Значение fileName, полученное при помощи функции getArgs, напечатать нельзя, потому что в обработчик передаётся только значение типа IOException и он не знает ни о чём другом. Функция зависит только от своих параметров. Но мы можем вызвать функцию ioeGetFileName, которая по переданному ей исключению возвращает Maybe FilePath. Функция пытается получить из значения исключения имя файла, если такое возможно. Давайте изменим обработчик так, чтобы он печатал полное имя файла, из-за которого возникло исключение (не забудьте включить функцию ioeGetFileName в список импорта для модуля System.IO.Error):
handler :: IOException -> IO ()
handler e
| isDoesNotExistError e =
case ioeGetFileName e of
Just fileName -> putStrLn $ "Файл " ++ fileName ++
" не существует!"
Nothing -> putStrLn "Файл не существует!"
| otherwise = ioError e
where fileName = ioeGetFileName e
В охранном выражении, если предикат isDoesNotExistError вернёт значение True, мы использовали выражение case, чтобы вызвать функцию ioeGetFileName с параметром e; затем сделали сопоставление с образцом по возвращённому значению с типом Maybe. Выражение case часто используется в случаях, когда вам надо сделать сопоставление с образцом, не создавая новую функцию. Посмотрим, как это сработает:
$ ./linecount dont_exist.txt
Файл dont_exists.txt не существует!
Вы не обязаны использовать один обработчик для перехвата всех исключений в части кода, работающей с системой ввода-вывода. Вы можете перекрыть только отдельные части кода с помощью функции catch или перекрывать разные участки кода разными обработчиками, например так:
main = do
action1 `catch` handler1
action2 `catch` handler2
launchRockets
Функция action1 использует функцию handler1 в качестве обработчика, а функция action2 использует handler2. Функция launchRockets не является параметром функции catch, так что любое сгенерированное в ней исключение обрушит нашу программу, если только эта функция не использует try или catch внутри себя для обработки собственных ошибок. Конечно же, action1, action2 и launchRockets – это действия ввода-вывода, которые «склеены» друг с другом блоком do и, вероятно, определены где-то в другом месте. Это похоже на блоки try–catch в других языках: вы можете поместить всю вашу программу в один блок try–catch или защищать отдельные участки программы и перехватывать различные исключения для разных участков.