Обработка ошибок и управление ресурсами

Конструкция try...with

Последнее обновление: 05.01.2024

Иногда при выполнении программы возникают ошибки, которые трудно предусмотреть или предвидеть, а иногда и вовсе невозможно. Например, при передачи файла по сети может неожиданно оборваться сетевое подключение. Такие ситуации называются исключениями. Язык 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

Поскольку 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# и связывает аргументы со свойствами типа.

идентификатор

Назначает имя для объекта исключения. Эквивалентно выражению :? System.Exception asидентификатор

идентификатор when условие

Выполняет сопоставление, если условие после when возвращает true.

Посмотрим на возможные варианты. Например, при делении на ноль генерируется исключение типа 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.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850