Конец трассировки внутреннего стека исключений что это
Перейти к содержимому

Конец трассировки внутреннего стека исключений что это

  • автор:

Exception Dispatch Info. Throw Метод

Некоторые сведения относятся к предварительной версии продукта, в которую до выпуска могут быть внесены существенные изменения. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

Перегрузки

Создает исключение, представленное текущим ExceptionDispatchInfo объектом, после восстановления состояния, сохраненного при захвате исключения.

Создает исключение источника, сохраняя исходные данные Watson и дополняя, а не заменяя исходную трассировку стека.

Throw()

Создает исключение, представленное текущим ExceptionDispatchInfo объектом, после восстановления состояния, сохраненного при захвате исключения.

public: void Throw();
public void Throw ();
member this.Throw : unit -> unit
Public Sub Throw ()

Комментарии

При восстановлении исключения в трассировку стека вставляется следующая строка, указывающая точку восстановления: «End of stack trace from the previous location where the exception was thrown» . Это похоже на то, как внутренние исключения или маршалированные исключения указываются в трассировках стека.

Что такое трассировка стека?

Трассировка стека в выводе — это отчет, в котором содержится информация о вложенности внутри функций относительно момента, когда произошел выброс исключения. Чтобы лучше понять, рассмотрим пример:

const func1 = () =>  throw Error('func1 error!'); >; const func2 = () =>  func1(); >; const func3 = () =>  func2(); >; try  func3(); > catch (e)  console.log(e); > // Вывод будет следующим (вместо anonymous будет имя файла с кодом, я запускал в консоли браузера) // Error: func1 error! // at func1 (:3:9) // at func2 (:8:3) // at func3 (:13:3) // at :17:3 

В коде у нас три функции, каждая из которых вызывает другую, последняя ( func1 ) внутри выбрасывает ошибку. Получается такая цепочка вызовов: func3 -> func2 -> func1 . Внутри блока try catch мы поймали ошибку и распечатали. По ней видно, что текст ошибки содержит не только то значение, переданное в Error() , но и имена функций, номера строк и позиций в строках, в которых была выброшена ошибка. Это и есть трассировка стека. По нему можно понять, что ошибка была внутри функции func1() , строка 3, позиция 9. Эта функция была вызвана внутри функции func2() , строка 8, позиция 3. Функция 2 была вызвана внутри func3() , строка 13, позиция 3. А функция func3() была вызвана на строке 17, позиция 3. Позиция указывает на номер символа (с которого начинается имя функции) с начала строки.

Стек вызовов и исключения

В этой статье продолжим разбор внутреннего устройства программ на .NET, поговорим о программном стеке вызовов и работе с ним, а также о возникновении исключений и их обработке.

Стек

Стек — структура данных, работающая по принципу LIFO(last in first out — последний вошел — первым вышел). Добавление элементов в стек происходит с одного конца, вершины стека. Удаление из стека происходит также, начиная с вершины стека, то есть с последнего добавленного элемента по порядку.

Принцип работы стека можно сравнить с обоймой — доступ мы можем получить только к верхнему элементу в стеке, для доступа к добавленному перед ним, необходимо удалить данный верхний элемент.

В C# стек представлен в виде коллекции Stack из пространства имен System.Collections и универсальной версией Stack из System.Collections.Generic .

Таким образом, для стека обязательно должны реализовываться три основных метода:

  • Push — добавление нового элемента в вершину;
  • Pop — получение верхнего элемента при его удалении;
  • Peek — чтение верхнего элемента без удаления.
var stack = new Stack(); stack.Push(1); stack.Push(2); stack.Push(3); Console.WriteLine(stack.Peek()); //3 Console.WriteLine(stack.Pop()); //3 Console.WriteLine(stack.Peek()); //2 stack.Pop(); Console.WriteLine(stack.Peek()); //1

Программный стек

У нас уже заходила речь в одной из предыдущих статей о том, что на стеке размещаются значение переменной типа значения, если оно является локальной переменной/аргументом/возвращаемым значением метода, а также ссылки на объекты в куче (если объект не является частью ссылочного типа).

Так вот для этих целей используется программный стек выполнения — заранее выделяемый под каждую программу участок памяти размером обычно 1 мб, который организован по принципу структуры данных стек.

Таким образом, при каждом вызове метода .NET инициализирует стек-фрейм (кадр стека, можно условно ассоциировать с контейнером), где и хранится вся необходимая информация для выполнения метода: параметры, локальные переменные, служебная информация, которая используется вызванной функцией, чтобы в нужный момент возвратить управление вызвавшей функции. Каждый такой новый фрейм (контейнер вызваемого метода) помещается на вершину стека и удаляется после завершения выполнения метода. У программного стека также есть особенность, что он растет в адресном пространстве памяти не снизу вверх, а сверху вниз.

Пимер программы и упрощенное представление стека вызовов

Мы не будем сейчас вдаваться в подробности устройства стека вызовов. На текущем этапе лишь важно понимать, что стек вызовов служит для хранения порядка выполнения кода, то есть для хранения адресов возврата в порядке вызова методов, которые впоследствии используются для возврата из метода, а также локальные переменные. При вызове метода фрейм метода добавляется на вершину стека, а при его завершении удаляется. Таким образом, программа завершается после после того, как цепочка вызова всех методов становится пустой, то есть программа будет работать до тех пор, пока стек вызовов не станет пустым.

StackTrace и StackFrame

При работе программы могут возникать непредвиденные ошибки, исключения (о них мы поговорим чуть дальше), например деление на ноль. И для того, чтобы программист исправил возможную свою ошибку в коде или обработал исключение, ему неплохо было бы вообще понять место возникновения данной «ошибки». В этих целях и полезен стек вызовов, откуда можно достать всю необходимую служебную информацию.

В C# для получения информации о стеке существует класс StackTrace из пространства имен System.Diagnostics , а за кадр (фрейм) стека отвечает класс StackFrame .

Трассировка стека — цепочка вызова методов, которые находятся в стеке на данный момент. То есть в любой момент выполнения кода мы можем рассмотреть состояние стека вызовов, что и будет являться трассировкой. Каждый же кадр стека знает кто его вызывает, но не знает о существовании кадров, которые будет порождать он, так как на момент обращения к текущему кадру, кадров выше не существует (они либо уже выполнены, либо еще не созданы).

using System.Diagnostics; class Program < public static void Method1() < Method2(); >public static void Method2() < Method3(); >public static void Method3() < StackTrace currentTrace = new StackTrace(true); Console.WriteLine( quot;Количество фреймов: "); for (int i = 0; i < currentTrace.FrameCount; i++) < StackFrame frame = currentTrace.GetFrame(i); Console.WriteLine(); Console.WriteLine( quot;Метод из файла " + quot;расположен на строчке "); > > public static void Main(string[] args) < Method1(); >>

В данном примере в методе Method3() мы создаем объект трассировки StackTrace , а затем получаем поочередно все фреймы стека, начиная с верхнего, нулевого, и заканчивая нижним фреймом метода Main . Таким нехитрым образом получается достать много полезной информации о нашем коде, в данном примере мы выводим имена цепочки вызывающих методов и их расположение, с помощью методов объекта фрейма GetFileName() и GetFileLineNumber() .
Нужно отметить, что для того, чтобы объект класса StackTrace собрал дополнительную инфу о расположении методов в коде необходимо использовать конструктор с булевым параметром fNeedFileInfo и передать в него true , иначе он не соберет данную информацию и мы получим на выходе методов GetFileName() и GetFileLineNumber() значения по умолчанию ( null и 0 соответственно).

Но не будем сейчас подробно останавливаться на стек трейсе об этом мы еще успеем поговорить в теме рефлексии. Это все был важный материал для понимания работы исключений, о которых далее и пойдет речь)

Ошибки и исключения

При разработке программ и их поддержке разработчики так или иначе сталкиваются с различными проблемами и ошибками кода. В одних случаях проблемы возникают из-за «плохого» кода, из-за чего работа приложения будет некорректной, а в других — из-за некорректного пользовательского ввода данных, которые не были учтены в коде приложения. Так или иначе приложение начинает приложение начинает при возникновении данных проблем начинает работать не так, как задумано, и программисту необходимо данные ошибки находить и исправлять/обрабатывать.

При разработке программного обеспечения мы сталкиваемся с тремя основными типами ошибок:

  • Программные ошибки, баги (bug — жук:) — ошибки в коде, допущенные программистом, приводящие к некорректной работе программы. Например, при обращении к элементам массива в цикле происходит выход за его пределы. Баги могут быть как простые вроде приведенного примера, так и с нарушением более глобальной сложной логики программы, и должны исправляться в процессе отладки кода программы.
  • Пользовательские ошибки — ошибки, которые допускает конечный пользователь во время работы с программой. Например, некорректный пользовательский ввод в текстовое поле, в котором ожидался ввод почты пользователя, а получили набор цифр, что может привести к возможным ошибкам в дальнейшей работе с программой. Такие проблемы обычно решаются путем добавления валидации входных данных.
  • Исключения — ситуации, которые не предусмотрены стандартным поведением программы. Например, когда коде программы мы открываем несуществующий файл для чтения из него или пытаемся подключиться к web-ресурсу, который по какой-то причине недоступен (его забанило правительственное ведомство нашего государства, а у нас не включен vpn:). Такие случаи обычно не зависят от программиста, и при их возникновении, чтобы программа не «падала» от непонимания, что делать дальше, эти исключительные ситуации необходимо обрабатывать.

В рамках .NET CLR будет генерировать соответствующие исключения даже для программных и пользовательских ошибок, которые не учел программист.

В языках, где механизмы для работы с исключениями не стандартизированы, программистам приходится придумывать свою логику для обработки ошибок (например, тот же C). Причем данная логика у всех команд разработчиков будет так или иначе своей, что несет за собой дополнительные сложности при понимании кода и вкатывании в проект. Одним из таких подходов обработки ошибок на языке C, где нет для этого стандартных средств, являлось определение числовых констант кодов ошибок, с помощью макросов (через директиву #define), и булевых флагов. В случае возникновения ошибки — функция возвращала определенный числовой код, и по полученному коду программистом выстраивалась логика действий по обработке. В случае с булевым флагом — флагу в глобальной области видимости присваивается соответствующие значение, если произошла ошибка, и на основании этого, после завершения функции, выстраивается логика по обработке.

/* Пример обработки ошибок в С, с помощью кодов */ #define CONN_FAIL 400 //объявление кода ошибки int ConnDatabase() < //Здесь происходит какая-то логика по подключению к базе, //но по какой-то причине могла возникнуть ошибка подключения, //в результате чего возвращается следующее значение return CONN_FAIL; >int main() < int res = ConnDatabase(); if (res == CONN_FAIL) < printf("Fail connection to db"); >return 0; >
/* Пример обработки ошибок в С, с помощью булевого флага */ bool error; double Div(double a, double b) < if (b == 0) < error = true; return NULL; >return a / b; > int main() < error = false; double res = Div(5, 0); if (error == true) < printf("Zero division error"); >else < printf("Result: %f", res); >return 0; >

При таком подходе в рамках одного проекта может быть определена куча таких кодов, с которыми придется работать присоединяющимся к проекту новым программистам. Но проблема кроется не только в том, что кодов может быть много, но и в том, что нет никакой стандартизации их объявления, то есть числовые коды одного проекта могут обозначать совершенно другие ситуации в другом, а при подключении какой-то библиотеки к проекту они и вовсе могут совпасть, но не по смыслу:)

Для недопущения этого безобразия, в C#, как в любом нормальном современном япе, есть стандартные средства для генерации и обработки исключений, что не может не радовать, а генерируемые исключения являются объектами, которые содержат в себе различную полезную информацию о том, что произошло и где, в отличие от непонятных числовых кодов (документация по которым в сделку проекта естественно не входила:)

Исключения в C#

Как уже было сказано ранее, исключение — ситуация, которая не предусмотрена стандартным поведением программы. В C# все исключения происходят от базового класса System.Exception , а все остальные классы исключений – это производные классы, которые позволяют конкретизировать тип возникшего исключения.

Независимо от того, какое исключение было сгенерировано, данное исключение будет иметь функциональность, которая наследуется от базового класса Exception . Таким образом все исключения имеют ряд полезных свойств, в которых отражается информация о причинах возникновения исключения и места:

  • Message — свойство с текстовым описанием ошибки, использующееся только для чтения и устанавливающееся в конструкторе;
  • HelpLink — свойство для указания или получения URL-ссылки для доступа к веб-странице с подробным описанием ошибки;
  • Source — свойства служащее для указания названия сборки или объекта, в котором возникло исключение;
  • TasrgetSite — свойство, доступное только для чтения, в котором содержится различная информация о методе, в котором возникло исключение;
  • StackTrace — свойство, доступное только для чтения, которое содержит цепочку вызовов методов до возникновения исключения;
  • InnerException — свойство, доступное только для чтения, которое может использоваться для получения информации о предыдущих исключениях, которые послужили причиной возникновения текущего исключения. Запись предыдущих исключений осуществляется путем их передачи конструктору самого последнего сгенерированного исключения.

Генерация исключений

При возникновении определенных системных ошибок при работе программы CLR самостоятельно генерирует соответствующие исключения. А все неперехваченные исключения приводят к аварийному завершению программы, и скорее всего вы уже сталкивались с данными ситуациями.

Вот простейший пример деления на 0, в результате чего CLR выбросит исключение:

class Program < public static int Div(int a, int b) < return a / b; >public static void Main() < Console.WriteLine(Div(5, 0)); Console.WriteLine(Div(5, 1)); >>

Код выше будет падать из-за вызванного неперехваченного исключения в первом вызове метода Div() при делении в нем на 0. В результате в консоли мы получим сообщение с информацией об исключении и стектрейсе, из которого мы можем понять, что было вызвано исключение DevideByZeroException в строке 5 метода Div() , при его вызове из строки 10 в файле Program.cs . Умение читать такие сообщения со стектрейсом является важным навыком для поиска ошибок в коде.

Для того, чтобы сгенерировать исключение вручную используется ключевое слово throw с объектом исключения.

//создаем объект исключения var ex = new Exception("Wow, its exception message"); //генерируем(выбрасываем) исключение throw ex;

Но нам не обязательно нужно сначала создавать объект исключения, а лишь потом его выбрасывать, обычно это делается в то же время:

throw new Exception("Wow, its exception message");

Мы можем в своих методах генерировать любые исключения классов, унаследованных от Exception , это при дальнейшем анализе дает больше конкретики о том, что произошло. В предыдущем примере с делением CLR сгенерировал исключение DivideByZeroException , что сразу говорит о произошедшим, в то время как я показал генерацию базового Exception с непонятным сообщением (не надо так делать:).

Давайте возьмем пример с делением и бросим подходящее исключение самостоятельно с каким-то логичным сообщением о произошедшем, опередив CLR:

class Program < public static int Div(int a, int b) < if (b == 0) throw new DivideByZeroException("Division by 0 is not allowed. "); return a / b; >public static void Main() < Console.WriteLine(Div(5, 0)); >>

Предлагаю еще рассмотреть еще одну ситуация с генерацией и переходим к обработки исключений.

Допустим, мы в методе используем в качестве параметра ссылочный тип и передаем при вызове в качестве аргумента ссылку на null . В таком случае подходящим типом для исключения будет ArgumentNullException , который в конструкторе принимает в качестве первого параметра имя параметра, с которым произошла проблема:

class Program < public static void Hello(string name) < if (name == null) throw new ArgumentNullException(nameof(name), "Name must not be null or empty"); Console.WriteLine( quot;Hello "); > public static void Main() < Hello(null); >>

Ситуация с null действительно типовая и часто встречается в начале общедоступных методов для проверки на возможное нежелательное значение параметров. Из-за того, что таких параметров могло быть много и производилась куча проверок, загрязняя код, в .NET 6 классу ArgumentNullException был добавлен статический метод ThrowIfNull , который генерирует соответствующее исключение, если параметр равен null .

class Program < public static void Hello(string name, string id) < ArgumentNullException.ThrowIfNull(name, nameof(name)); ArgumentNullException.ThrowIfNull(id, nameof(id)); Console.WriteLine( quot;Hello with "); > public static void Main() < Hello("Artem", null); >>

Согласитесь, это выглядит гораздо компактнее)

Обработка исключений

Как уже было сказано, неперехваченные исключения приводят к аварийному завершению программы, так что вызывать код генерирующий исключение и никак его не обрабатывать — не круто. Для перехвата и обработки исключений используется конструкция try catch finally .

try < // Блок с потенциально проблемным кодом, // который может сгенерировать исключение >catch(Exception ex) < // Блок перехвата исключения, // если оно было сгенерировано >finally < // Финальный блок для закрытия каких-либо ресурсов, // который выполняется всегда в независимости, // было вызвано исключение или нет >

Алгоритм работы данной конструкции сводится к следующему:

  • В блок try помещается код, который в ходе выполнения может привести к возникновению исключений.
  • Если в блоке try возникает исключение, то CLR останавливает выполнение инструкций в try и начинает искать подходящий блок catch , который предназначен для обработки данного исключения. Для блока catch мы указываем тип перехватываемого исключения, а внутри блока уже прописываем необходимые инструкции, которые необходимо выполнить для обработки исключения, например, зафиксировать каким-либо образом сообщение об ошибке: вывести в лог, отправить сообщение о проблеме на почту или в телегу.
  • Блок finally выполняется в конце всегда независимо, было вызвано исключение или нет, за исключением пары случаев, но о них позже.
  • Если при вызванном исключении оно было успешно «отловлено» с помощью catch , после блока finally выполнение переходит на следующую за данным блоком инструкцию.

Например, вот так мы можем отловить исключение при нашем делении на 0, и вывести сообщение об ошибке.

class Program < public static int Div(int a, int b) < if (b == 0) throw new DivideByZeroException("Division by 0 is not allowed. "); return a / b; >public static void Main() < try < int res = Div(5, 0); Console.WriteLine( quot;Result: "); > catch (DivideByZeroException ex) < Console.WriteLine( quot;Error: "); > finally < Console.WriteLine("End divide"); >Console.WriteLine("End program"); > >

Следует выделить несколько правил конструкции try catch finally :

  1. При использовании try всегда должен использоваться как минимум один catch либо finally ;
  2. Блоков catch может быть много, либо ни одного, но они не могут существовать без блока try .
  3. Блок finally не является обязательным при использовании try catch , но, если он используется, то он может быть только один.

То есть при наличии блока catch мы можем опустить блок finally , и, наоборот, при наличии блока finally мы можем опустить блок catch и не обрабатывать исключение. Но если мы не обработаем исключение, то блок finally отработает, а дальше приложение аварийно завершит работу, в случае возникновения исключения, как уже говорилось ранее.

Так как в предыдущем примере нам не нужна какая-то дополнительная логика для закрытия ресурсов, то и блок finally нет никакого смысла использовать в данном случае и его можно опустить:

try < int res = Div(5, 0); Console.WriteLine( quot;Result: "); > catch (DivideByZeroException ex) < Console.WriteLine( quot;Error: "); > Console.WriteLine("End program");

В реальном коде код внутри блока try может быть гораздо сложнее и генерировать более одного исключения. Для обработки каждого случая можно использовать множество блоков catch для каждого конкретного типа исключения.

Давайте добавим в наш пример возможность считывания чисел для деления с клавиатуры:

try < Console.Write("Enter numerator: "); int a = int.Parse(Console.ReadLine()); Console.Write("Enter denumerator: "); int b = int.Parse(Console.ReadLine()); int res = Div(a, b); Console.WriteLine( quot;Result: "); > catch (DivideByZeroException ex) < Console.WriteLine( quot;Error: "); >

Теперь при вводе в консоле не чисел для числителя и знаменателя метод Parse() будет генерировать исключение FormatException , которое не будет перехвачено с помощью блока catch , т.к. он перехватывает в данном случае только DivideByZeroException , и программа аварийно завершится в таком случае. Чтобы этого избежать, необходимо добавить блок catch для обработки данных исключений:

try < Console.Write("Enter numerator: "); int a = int.Parse(Console.ReadLine()); Console.Write("Enter denumerator: "); int b = int.Parse(Console.ReadLine()); int res = Div(a, b); Console.WriteLine( quot;Result: "); > catch (DivideByZeroException ex) < Console.WriteLine( quot;Error: "); > catch (FormatException ex) < Console.WriteLine( quot;Error: "); Console.WriteLine("You entered not a number. "); > catch (Exception ex) < Console.WriteLine( quot;Unreported error: "); >

Но при написании нескольких блоков catch нужно учитывать, что сгенерированное исключение будет обрабатываться первым подходящим боком catch , то есть блоки catch должны быть расположены в своей последовательности так, чтобы первый catch перехватывал наиболее специфическое исключение (то есть производный тип, расположенный ниже всех в цепочке наследования типов исключений), а последний catch — самое общее исключение (то есть базовый класс Exception ). Если же расположить catch с более базовым классом перед остальными, например Exception , то это будет ошибка компиляции и у нас ничего не получится, так как блоки catch за ним будут являться недостижимыми.

Исключения в стеке вызовов

При возникновении исключения в ходе выполнения программы CLR отвечает за поиск соответствующего обработчика catch . Для этого среда CLR в обратном порядке начинает проходить через стек вызовов, начиная с фрейма текущего метода, в котором возникло исключение, т.к. он находится на вершине стека. Если CLR находит подходящий обработчик, то передает ему объект исключения, а если нет, то переходит к предыдущему фрейму вверх по стеку. Если же, пройдя через весь стек вызовов, CLR не найдет подходящий обработчик, она выполнит обработку исключения по умолчанию и аварийно завершит работу программы.

Разберем на примере из 4-х методов, которые вызываются по цепочке, и в последнем возникает исключение:

class Program < public static void Method1() < try < Method2(); >catch(NotImplementedException ex) < Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); Console.WriteLine(); >Console.WriteLine("Продолжаем выполнение"); > public static void Method2() < Method3(); Console.WriteLine("Это сообщение не напечатается"); >public static void Method3() < try < throw new NotImplementedException("Исключение из метода 3"); >catch(IOException ex) < Console.WriteLine("Тут не поймайется исключение"); >> public static void Main() < Method1(); >>

Method3() находится на вершине стека вызовов, когда в нем возникает исключение NotImplementedException . CLR не найдет подходящего catch в Method3() и перейдет к следующему блоку try в стеке вызовов, предварительно удалив поочередно с вершины стека фреймы Method3 и Method2 , добавляя информацию о них в стек трейс исключения. В Method1() найдется подходящий обработчик, поэтому выполнение продолжится с него.

Таким образом, исключения являются довольно затратным механизмом и к ним лучше прибегать в крайнем случае, а если все же применяем, то не нужно тянуть исключение через весь стек, лучше его обрабатывать в том же методе, который и вызывает проблемный код.

Повторная генерация исключений

Иногда все же бывает необходимо обработать исключение, а затем, повторно сгенерировав, «отправить» вверх по стеку. Но способов повторной генерации существует несколько, и выбор «неправильного» приводит к изменению информации об исключении в StackTrace , что будет весьма затруднять поиск проблемы и путать.

Один из вариантов, использовать обычную генерацию исключения с помощью throw ex , с перехваченным объектом исключения ex .

class Program < public static void Method1() < try < Method2(); >catch(NotImplementedException ex) < throw ex; >> public static void Method2() < Method3(); >public static void Method3() < throw new NotImplementedException("Исключение из метода 3"); >public static void Main() < try < Method1(); >catch(Exception ex) < Console.WriteLine(ex.TargetSite); Console.WriteLine(ex.StackTrace); >> >

Такой подход будет заново генерировать исключение, то есть для него вся служебная информация о стек трейсе исключения будет новой, начиная от строки, где повторно было сгенерировано исключение. Хотя по факту мы могли ожидать информацию об исключении из Method3() , что может ввести в заблуждение.

Для того чтобы бросить текущее перехваченное исключение дальше можно использовать ключевое слово throw без объекта исключения, в таком случае информация будет та, которую мы и ожидали.

public static void Method1() < try < Method2(); >catch(NotImplementedException ex) < throw; >>

Но такой вариант не дает полной картины, если необходимо получить информацию, включая повторную генерацию. В таком случае мы можем создать новый объект исключения, обернув в него исходное:

public static void Method1() < try < Method2(); >catch(NotImplementedException ex) < throw new Exception("Rethrown", ex); >>

В Main() изменил вывод на полный Console.WriteLine(ex)

Создание собственных классов исключений

В .NET уже содержится множество системных исключений, генерируемых исполняющий средой, и которые можно использовать в своих целях. Но в некоторых ситуациях бывает нужно отделить возникающую проблему в приложении от системных исключений. В таком случае можно создать собственный класс исключений.

Для создания собственного класса исключений необходимо и достаточно унаследовать свой класс от System.Exception или производного от него (о наследовании мы поговорим позже в лекциях об ООП:). Но есть четкая иерархия исключений, которой стоит придерживаться. System.Exception является базовым общим классом для всех создаваемых исключений, от него напрямую наследуются классы System.SystemException и System.ApplicationException . SystemException определяет исключения, генерируемые самой платформой .NET, а ApplicationException определяет исключения, порождаемые приложением.

Таким образом, Microsoft рекомендуют собственные классы исключений наследовать именно от ApplecationException . Как уже было сказано, чтобы создать класс исключения достаточно его просто унаследовать от Exception или от унаследуемого от него.

public class HouseOnFireException : ApplicationException

Но это не является хорошей практикой, т.к. мы не сможем передать даже никакое сообщение при генерации исключения. Для решения этой проблемы необходимо задать конструкторы, которые будут при создании объекта исключения обращаться к конструктору базового класса, с помощью base (о нем мы также подробнее поговорим уже в курсе лекций по ООП:)

public class HouseOnFireException : ApplicationException < public HouseOnFireException() < >public HouseOnFireException(string message) : base(message) < >public HouseOnFireException(string message, Exception innerException) : base(message, innerException) < >>

Все, теперь мы можем генерировать исключение нашего класса:)

Как-то так:) В следующей лекции поговорим еще раз про типизацию и разберем основы приведения типов, и наверно закончим с базой шарпа. А далее начнем курс лекций по ООП. Так что подписывайтесь на тг канал , ставьте лайки и вступайте в наш чат, там ничего нет.

Stack Trace и с чем его едят

В этой статье вы узнаете и поймете, как работает такое явление в Java, как StackTrace, так же известное как «Трассировка стека вызовов». Эта информация была структурирована для новичков, столкнувшихся с этим понятием в начале девятого уровня Java Syntax. Я думаю все из вас, хоть раз, но встречали похожие ошибки при работе в вашем IDE, независимо от того будь это Idea, Eclipse или что-то другое.

Exception in thread "main" java.lang.ArithmeticException at com.example.task01.Test.division(Test.java:10) at com.example.task01.Test.main(Test.java:6)

Принцип работы коллекции Stack

Это, как вы уже догадались и есть наша трассировка. Но не спешите паниковать, сейчас мы с вами разложим данный пример на пальцах. Для начала необходимо понять тот факт, что StackTrace работает как Стэк и это видно из его названия. На этом месте мы остановимся чуть поподробнее. На восьмом уровне вы уже познакомились с коллекциями и знаете что они делятся на три группы Set — множество, List — список, Map — словарь (или карта). По мнению JavaRush (c). Наш Stack входит в группу List . Принцип его работы можно описать как LIFO, что расшифровывается как Last In First Out(Последний пришел, первый ушел). А именно это такой список похожий на стопку книг, чтобы взять элемент который мы положили в Stack первым, нам необходимо сначала извлечь все элементы которые мы добавили в наш список после. Как это указано на картинке выше в отличии например от обычного списка ArrayList где мы можем получить любой элемент из списка по индексу. Еще раз для закрепления. Получение элемента из Стэка возможно только с конца! В то время как первый добавленный в него элемент находится в начале(или на дне как удобнее). Вот какие методы имеет наш Stack Object push() — Добавляет элемент в верх стека. Object pop() — Возвращает элемент, находящийся в верхней части стэка, удаляя его в процессе. Object peek() — Возвращает элемент, находящийся в верхней части стэка, но не удаляет его. int search() — Ищет элемент в стеке. Если найден, возвращается его смещение от вершины стека. В противном случае возвращается -1. boolean empty() — Проверяет, является ли стек пустым. Возвращает true, если стек пустой. Возвращает false, если стек содержит элементы. Так для чего же в Java нужен StackTrace построеный на принципах работы Stack ? Давайте разберем пример ошибки ниже, которая возникла в процессе выполнения такой вот простой программы.

Java-университет

public class Test  public static void main(String[] args)  System.out.println(convertStringToInt(null)); > public static int convertStringToInt(String s)  int x = Integer.parseInt(s); return x; > >

У нас есть класс Test с двумя методами. Всем привычный main и convertStringToInt логика которого заключается в конвертировании и возврате полученной извне(а именно из метода main ) строки в целочисленное число типа int . Как вы видите мы намеренно передали вместо строки с какой-нибудь цифрой, параметр null . Данный параметр наш метод не смог правильно обработать и вызвал ошибку NumberFormatException . Как вы знаете программа начинает отрабатывать свою работу из метода main и в этот момент она создает новый Стэк с названием StackTrace куда кладет текущее значение ее работы под номером 1, далее мы переходим в метод convertStringToInt и программа опять заносит параметры нашего нахождения в созданный ранее StackTrace под номером 2, далее вызывается не видимый нашему глазу метод parseInt находящийся в классе Integer и это уже будет элемент под номером 3 нашего StackTrace , в этом методе будет еще один внутренний вызов добавленный в StackTrace под номером 4 для проверки элемента на null который и приведет к ошибке. Программе необходимо вывести нашу ошибку с указанием всей цепочки наших переходов до момента возникновения ошибки. Тут то ей и приходит на помощь ранее созданный StackTrace с данными наших переходов.

Exception in thread "main" java.lang.NumberFormatException: null at java.base/java.lang.Integer.parseInt(Integer.java:614) at java.base/java.lang.Integer.parseInt(Integer.java:770) at com.example.task01.Test.convertStringToInt(Solution.java:10) at com.example.task01.Test.main(Solution.java:6)

До возникновения ошибки, программа шла вглубь методов, но как только возникла ошибка, все начинает происходить в обратном порядке. Печатается строка с описанием проблемы(№1 на примере), далее берется последнее (и находящееся на вершине) добавленное значение в наш Стэк оно было под номером четыре и печатается в консоль(№2 на примере) и мы видим что проблема возникла в классе Integer на 614 строке кода и вызвала эту строку, строка 770 метода parseInt того же класса(№3 на примере) которая при добавлении в Стэк была под номером три и этот метод класса Integer все еще не видимый нам был вызван уже нашим методом convertStringToInt располагающемся на 10 строке нашей программы(№4 на примере, а при добавлении он был вторым), а его в свою очередь вызвал main на 6 строке(№5 на примере, а при добавлении соответственно первый). Вот так вот, складируя в Стек шаг за шагом наши вызываемые методы мы смогли вернуться обратно в main параллельно печатая информацию что именно привело нас к ошибке. Но StackTrace это не только работа с ошибками, он позволяет получить нам кучу интересной информации о процессе работы нашего приложения. Давайте разберем еще один популярный пример в комментариях к основной лекции 9го уровня. У нас есть код и к нему сразу прикреплю картинку визуализирующую процесс работы программы:

public class Test  public static void main(String[] args)  method1(); method2(); > public static void method1()  //не вызывает ничего > public static void method2()  method3(); method4(); > public static void method3()  //не вызывает ничего > public static void method4()  method5(); > public static void method5()  StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); for (StackTraceElement element:stackTraceElements)  System.out.println(element.getMethodName()); > > >

Stack Trace и с чем его едят - 2

Тут наша программа безошибочно выполняет свою работу и заканчивается. Вот что мы увидим в выводе консоли:

getStackTrace method5 method4 method2 main Process finished with exit code 0 

Как у нас получился такой вывод и что же произошло в пятом методе начиная с 20й строки? Боюсь самое лучше что я смогу сделать это добавить самое популярное объяснение(в сокращении) юзера Кирилла из комментариев к лекции. Обратимся к строчке по созданию StackTrace и разберем ее поэлементно:

StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();

StackTraceElement[] — указание на тип массива(На ранних уровнях вы уже проходили массивы типа int[], String[], вот тут тоже самое). stackTraceElements — имя массива, может быть любым с учетом общих правил наименования на работу эту не влияет. Thread.currentThread() — получение ссылки на текущий поток, в котором выполняются методы, которые мы хотим отследить(пока это не важно, подробнее потоки вы будете разбирать на 16 уровне в квесте Java Core) getStackTrace() — получаем весь Стэк вызываемых методов(Это обычный геттер для StackTrace ) Теперь посмотрим, чем нам может быть полезен созданный массив. Мы понимаем, что в массиве хранится инфа о выполненных методах.(с) И для этого в 21й строке мы запускаем модифицированный цикл for под названием forEach (кстати кто еще не изучил этот цикл, советую почитать о нём) и выводим данные из массива в консоль, а именно информацию какие методы выполнялись в процессе работы посредством конструкции element.getMethodName() . Внимание как мы видим нулевым элементом массива у нас оказался сам getStackTrace() соответственно так как в момент получения массива данных он был последним методом что выполнился и тем самым оказавшись на верхушке Стэка , а помня про нашу конструкцию «Последний пришел, первый ушел» сразу же первым добавляется в массив под нулевым элементом. Вот что еще мы можем получить из StackTraceElement : String getClassName() — Возвращает имя класса. String getMethodName() — Возвращает имя метода. String getFileName() — Возвращает имя файла (в одном файле может быть много классов). String getModuleName() — Возвращает имя модуля (может быть null). String getModuleVersion() — Возвращает версию модуля (может быть null). int getLineNumber() — Возвращает номер строки в файле, в которой был вызов метода. Теперь, когда вы поняли общий принцип работы, советую вам самим опробовать разные методы StackTrace в вашей Ide. Даже если вы не совсем всё усвоили, продолжайте обучение и мозаика сложится так же как сложилась у меня в данном вопросе. Желаю вам всем успехов! P.s. Если вам понравился данный материал, пожалуйста поддержите лайком. Вам не трудно, мне приятно. Спасибо и увидимся на 41 уровне 😉

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *