Предыдущие статьи:
Итак, мы изучили условия if ... else, циклы 4 видов:
- foreach ("для каждого"),
- for ("для", цикл со счётчиком),
- while ("пока"),
- do ... while ("делать ... пока", "пока с постусловием").
Но кроме условий и циклов, в современном программировании для написания логики используется конструкция try ... catch ... finally. Причём в некоторых проектах она используется даже чаще, чем циклы.
Что такое исключения в C#?
Конструкция try ... catch ... finally работает с исключениями, поэтому сначала надо понять, что это такое.
Исключением называется любая ошибка в работе программы, в результате которой она "падает" (то есть закрывается). Например, разделим на 0 и запустим программу в режиме отладки - она автоматически остановится, когда будет выброшено исключение:
Во-первых, исключение показывается на полях в виде молнии. Во всплывающем окне указаны: красное - класс исключения (да, те самые классы, которые мы можем создавать сами), жёлтое - сообщение с подробностями, зелёное - файл и номер строки кода.
Другой пример - попытка обратиться к полю/методу объекта, когда вместо объекта в переменной хранится null (классика, часто возникает):
Если в одной команде ("строке") есть несколько доступов с точкой, то придётся угадывать, на каком именно из них программа упала. Например:
А если команда занимает несколько строк, то в сообщении печатается первая строка команды. К счастью, в Rider конкретное место может быть подсвечено:
Почему ошибки компиляции - это хорошо
Итак, исключения - это ошибки, возникающие именно во время работы программы, а не во время компиляции (сборки). Обычно ошибки компиляции подчёркиваются красным, а исключения - жёлтым (если повезёт). Ошибки компиляции, безусловно, лучше исключений, потому что программист узнаёт о них при первой же попытке запустить программу. Об исключениях можно не узнавать месяцами, пока у кого-нибудь из пользователей оно не возникнет. В строго типизированных языках, где каждая переменная имеет свой тип данных, ошибки при написании кода часто превращаются в ошибки несовместимости типов данных, что выявляется на стадии компиляции и программисты быстро устраняют такие ошибки. А вот в таких языках, как Python и JavaScript, не являющихся строго типизированными, проблема с этим - многие ошибки компиляции превращаются в исключения, которые не заметны сразу. У меня был случай, когда ошибка, которая была бы видна в C# при компиляции, в питоне не была заметна в течение почти недели. Программа успешно работала несколько дней, а потом внезапно падала. Отловить такую ошибку было сложно.
Конструкция try ... catch ... finally
Исключения можно "ловить" (catch), так что программа после них будет продолжать работу. Для отлавливания исключений используется try ... catch ... finally:
Блок try - это те команды, в которых могут возникать исключения. В нашем случае там находится деление, которое потенциально может приводить к исключению вида "деление на 0". В случае исключения выполнение этих команд прерывается, то есть, в случае деления на 0 печать результата Console.WriteLine(result) не будет выполнена.
Блоки catch - это отлов исключений определённого класса и команды, которые нужно выполнить в случае отлова. Пойманные исключения не приводят к падению программы (только прерывается блок try). В круглых скобках указан класс исключений и создаётся переменная e, куда будет положен конкретный объект этого класса (да, исключение - это объект!). В нашем случае первый блок catch отлавливает исключения класса DivideByZeroException (деление на 0), а второй блок catch отлавливает вообще любые исключения. Когда под исключение подходит несколько разных блоков catch, выполняется первый по порядку. То есть, при делении на 0 выполнится только первый catch, а второй не будет выполняться. Если соответствующего блока catch нет, то исключение будет выброшено наружу (мы поймём это, когда будем рассматривать стек вызовов). Если и там не окажется соответствующего блока catch, то программа упадёт с ошибкой.
Блок finally - команды, которые нужно выполнить в любом случае - независимо от того, было ли исключение или его не было, было ли оно отловлено или программа аварийно завершается. Даже если исключение не было отловлено и программа аварийно завершается, блок finally всё равно отработает. Например, если программа открыла файл, то даже в случае своего падения она обязана его закрыть. Поэтому подобные команды ставят в finally.
Блоки catch могут отсутствовать - тогда должен быть блок finally. И наоборот, блок finally может отсутствовать - тогда должен быть хотя бы один блок catch. Потому что один блок try без catch и finally ничего полезного не мог бы сделать.
Также нужно разобрать, почему печать результата в консоль находится в блоке try вместе с делением. Дело в том, что если поместить печать результата в код после try ... catch, то этот код будет выполняться всегда - и в случае нормального деления, и в случае отлова исключения. Но в случае исключения результата не будет и печатать нечего! К счастью, от такой ошибки нас охраняет сам синтаксис языка C# - переменная result объявлена внутри блока try, а значит, она видна только внутри его фигурных скобок. За блоком try её нет и обратиться к результату всё равно не получилось бы. Другими словами, в блок try помещается весь набор действий, который мы хотим выполнить, а в случае исключения их выполнение будет прервано.
Наконец, запустим программу и посмотрим, как печатается в консоль объект исключения e:
Кидание исключений
Бывают случаи, когда текущие действия невозможно выполнить и нужно прервать их и сказать, что произошла ошибка. Например, если программа пытается открыть файл со своими настройками, а его нет. Или если калькулятор пытается разделить на 0. Тогда мы можем использовать команду throw объектИсключения:
Команде throw передаётся объект исключения. Объект создаётся с помощью new НазваниеКласса(), как обычно. Обычно конструкторы всех исключений умеют получать на вход строку с сообщением. В данном случае мы можем указать русский человекочитаемый текст вместо программистского.
Мы можем создать свой собственный класс исключений. Просто создадим свой класс DivideOn100Exception ("ошибка деления на 100"). Класс исключения обязательно должен наследоваться (то есть основываться на) классе Exception, иначе его объекты нельзя будет кидать. Это указывает двоеточие:
Подробнее про наследование мы разберём потом в теме "Наследование и полиморфизм в ООП".
Теперь мы можем кинуть исключение своего класса:
Только на вход конструктору нельзя подать строку с сообщением, потому что свои конструкторы мы ещё не умеем писать.
Добавьте ещё одну секцию catch для отлова исключений нового класса. Проверьте, срабатывает ли она.
Далее
Оператор using - https://dzen.ru/a/aZRFxuVUCU1sgRcd?share_to=link
Оглавление - https://dzen.ru/a/aXisxwt_Mnz2qTjs?share_to=link