Иногда при выполнении программы возникают ошибки, которые трудно предусмотреть или предвидеть, а иногда и вовсе невозможно. Например, при передачи файла по сети может неожиданно оборваться сетевое подключение. Такие ситуации называются исключениями. Язык F# предоставляет разработчикам возможности для обработки таких ситуаций и для этого предоставляет ряд конструкций, в частности, конструкцию try...with
Конструкция try...with имеет следующий синтаксис:
try код, где может возникнуть исключение with | паттерн_1 -> выражение_1 | паттерн_2 -> выражение_2
Между try
и with
располагается код, который потенциально может сгенерировать исключение.
Конструкция try...with
возвращает значение. Если исключение не создается, то возвращается значение, которое определяется в
коде между try
и with
.
Если генерируется исключение, то приложение сравнивает возникшее исключение с каждым паттерном после слова with. Для каждого такого паттерна
определяется некоторое выражение, которое выполняется, если паттерн соответствует возникшему исключению. Подобное выражение еще называют
обработчиком исключения. А вся конструкция try...with
возвращает значение, которое возвращает обработчик исключения.
Однако может сложиться ситуация, если ни один паттерн не соответствует исключению. В этом случае исключение распространяется вверх по стеку вызовов, пока не будет найден соответствующий обработчик. Стоит отметить, что обработчики исключений должны возвращать значение того же типа, что и код в блоке try.
Рассмотрим хрестоматийный пример - деление числа:
let divide a b = a / b let result = divide 100 0 // ! генерируется исключение printfn "%d" result // далее код не выполняется let result2 = divide 100 20 printfn "result: %d" result2
Здесь функция divide принимает два числа и делит их. Далее мы можем вызвать эту функцию и получить результат. С точки зрения синтаксиса здесь пробле никаких нет, все прекрасно скомпилируется. Однако в процессе выполнения, посколько делитель равен 0, будет сгенерировано исключение. В итоге программа упадет, выдав на консоль соответствующую информацию:
C://Users/Eugene/Documents/fsharp>dotnet fsi program.fsx System.DivideByZeroException: Attempted to divide by zero. at <StartupCode$FSI_0001>.$FSI_0001.main@() in C://Users/Eugene/Documents/fsharp/program.fsx:line 7 at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) Остановлено из-за ошибки C://Users/Eugene/Documents/fsharp>
Поскольку программа завершается ошибкой, то весь код, который идет после строки, где возникло исключение, не срабатывает.
Теперь применим обработку исключения:
let divide a b = try a / b with | _ -> printfn "Division by zero!"; 0 let result = divide 100 0 // исключение обрабатывается printfn "result: %d" result // result: 0 let result2 = divide 100 20 printfn "result: %d" result2 // result: 5
Теперь код, который потенциально может сгенерировать исключение - a / b
, помещается в блок try
В блоке with
указываем шаблоны для сопоставления с возникшем исключением. В данном случае используем символ подстановки _,
который соответствует вообще любому исключению. И для этого паттерна просто выводим строку об исключении на консоль.
Кроме того, нам надо возвратить значение из блока with
, которое соответствует по типу возвращаемому значению из блока try
.
В данном случае для теста возвращаем 0. В итоге даже если и возникнет исключение, оно будет обработано, а программа продолжит свою работу. Консольный вывод:
C://Users/Eugene/Documents/fsharp>dotnet fsi program.fsx Division by zero! result: 0 result: 5 C://Users/Eugene/Documents/fsharp>
Хотя программа выше теперь не падает и успешно завершается, но она имеет недостаток - мы должны во всех выражениях в блоке with возвратить значение того же типа, что и блок try. Выше для теста возвращается число 0, однако это не лучший вариант, поскольку 0 может быть валидным результатом. Здесь же, получив результат, мы не сможем определить, возникла ли ошибка или нет. И стандартное рещение в данном случае - возвращать из конструкции try...with значение объединения option - вариант Some или None. Если исключения нет - возвращаем валидное значение, обернутое в Some. Если возникает исключение, то возвращаем None:
let divide a b = try Some(a / b) with | _ -> printfn "Division by zero!"; None let printResult result = match result with | Some(n) -> printfn "result: %d" n | None -> printfn "Error" let result = divide 100 0 // исключение обрабатывается printResult result // Error let result2 = divide 100 20 printResult result2 // result: 5
Поскольку try...with возвращает результат, то мы можем получить его как любое другое значение:
let printResult result = match result with | Some(n) -> printfn "result: %d" n | None -> printfn "Error" let a = 100 let b = 5 let c = 0 let result1 = try Some(a / b) with | _ -> None let result2 = try Some(a / c) with | _ -> None printResult result1 // result: 20 printResult result2 // Error
В блоке with мы можем использовать следующие паттерны:
Паттерн |
Описание |
:? тип_исключения |
Сопоставляет с указанным типом исключения .NET. |
:? тип_исключения as идентификатор |
Сопоставляет с указанным типом исключения .NET, но при этом устанавливает для объекта исключения имя. |
имя_исключения(аргументы) |
Сопоставляет исключение с типом F# и связывает аргументы со свойствами типа. |
идентификатор |
Назначает имя для объекта исключения. Эквивалентно выражению |
идентификатор when условие |
Выполняет сопоставление, если условие после |
Посмотрим на возможные варианты. Например, при делении на ноль генерируется исключение типа System.DivideByZeroException. Перехватим это исключение:
let divide a b = try Some(a / b) with | :? System.DivideByZeroException -> printfn "Division by zero!"; None
Тип исключения указывается после оператора :?. Однако если в блоке try возникнет исклчючение другого типа (что в коде выше маловероятно), то оно не будет обработано, и приложение аварийно завершится. Но мы можем дополнительно добавлять новые паттерны:
let divide a b = try // некоторый код with | :? System.DivideByZeroException -> printfn "Division by zero!"; None | :? System.ArgumentException -> printfn "Argument is invalid!"; None
Типов исключений много, и чтобы охватить все эти исключения, можно использовать паттерн _, либо сопоставлять с базовым типом всех исключений - System.Exception
Установка имени объекта исключения:
let divide a b = try Some(a / b) with | :? System.Exception as ex -> printfn "%s" ex.Message; None
В данном случае возникшее исключение сопоставляется с типом System.Exception и доступно через идентификатор ex. Используя этот идентификатор, мы можем обращаться к функциональности объекта исключения. В частности, у типов исключений есть свойство Message, которое хранит сообщение об исключении.
Можно указать только имя для объекта исключения:
let divide a b = try Some(a / b) with | ex -> printfn "%s" ex.Message; None
Здесь объект ex опять же представляет один из типов исключений .NET - типа System.Exception или производных типов
Можно добавить условие после оператора when:
let divide a b = try Some(a / b) with | ex when a = 0 -> printfn "Both numbers are zero"; None | ex -> printfn "%s" ex.Message; None
В данном случае первый паттерн срабатывает, если параметр a равен 0.