Andriy Shyrokoryadov

.Net developer, data scientist

Структурированная обработка исключений - вопрос №28 на собеседование C# / .NET

Текст к видео "Структурированная обработка исключений" на канале YouTube

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

Все ошибки, которые возникают в процессе работы нашего приложения можно поделить на 3 категории:

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

  • ошибки пользователя – это ошибки, которые допускают пользователи в процессе работы с приложением. Например, ввод текста в поле, которое предназначено для ввода телефона. В итоге мы ожидали цифры, но получили буквы и в приложении что-то пошло не так. Как правильно такие ошибки исправляются добавлением валидации данных, которые вводят пользователи.

  • исключения – как говорит название этого типа ошибка, речь идет о какой-то исключительной, как правило негативной, ситуации в приложении. Такие ситуации трудно, иногда практически невозможно, предвидеть, поэтому они время от времени возникают в приложении. Исключения могут быть двух типов: системные исключения и исключения, определенные программистом, то есть исключения приложения. Если мы посмотрим на документацию Microsoft, то при описании различных методов, указывается какие исключения могут возникать в процессе работы данного метода. То есть используя определенные методы мы можем подготовиться к возникновению определенных исключений и в случае их возникновения, предпринять определенные действия для минимизации их негативных последствий.

Для начала давайте рассмотрим 2 аспекта исключений, не прибегая к примерам кода:

  • как исключения обслуживались ранее, когда не существовал синтаксис для структурированного обслуживания исключений;

  • как в общем виде выглядит структурированное обслуживание исключений;

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

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

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

Предположим, у нас есть некоторый метод в процессе работы, которого может возникнуть исключение. Я преднамеренно говорю о методах, а не каких-то определенных частях кода, потому что в соответствии с практиками правильного программирования обслуживание исключений должно распространяться на весь код метода, а не его отдельные части. О практиках правильного программирования и «чистом» коде у меня будет снято видео на канале, поэтому не забывайте подписываться на канал, чтобы не пропустить обновления.

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

Давай более детально рассмотрим логику обслуживания исключений. Как правило в данной логике будет проблематично восстановить состояние приложения до исключения, однако мы можем сделать следующее:

  • записать в логах какое исключение возникло;

  • постараться записать в логах если это возможно какие данные привели к возникновению исключения;

  • прекратить работу метода путем возврата объекта по умолчанию (это такой шаблон проектирования), вернуть значение null или значение по умолчанию если тип возвращаемого значения значимый, а также мы можем выбросить исключение на уровень выше - в метод, который вызвал наш текущий метод, в котором возникло исключение.

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

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

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

Исключение — это объект определенного класса, которое наследует класс Exception, в зависимости от имплементации исключения могут наследовать классы SystemException и ApplicationException. Оба эти классы наследуют класс Exception. Конвенция предполагает, что каждый класс исключений будет в своем названии иметь слово Exception в конце. Также конвенция предполагает, что исключения, которые наследуют класс SystemException будут выбрасываться библиотеками Microsoft .Net, а исключения, которые будут наследовать ApplicationException, будут исключениями приложения и будут созданы программистом.

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

  • Message – это текстовое описание исключения – данную информацию можно добавить в лог приложения, возможно это облегчит расследование возникновения исключения в будущем.
  • Data – набор значений «ключ-значение», который служит для передачи какой-то нестандартной информации об исключении.
  • StackTrace – это цепочка вызовов методов, которая позволяет нам определить какие методы были вызваны до момента пока не было выброшено исключение. Эта информация очень важная и желательно её не потерять в процессе обслуживания исключения.
  • HelpLink – иногда в этом свойстве указывается адрес интернет страницы, где можно поискать дополнительную информацию на тему выброшенного исключения.
  • InnerException – внутренне исключение: иногда наше текущее исключение является следствием возникновения другого исключения. В этом случае первоначальное исключение помещается в свойство InnerException.
  • Source – здесь будет указано название сборки или объекта в котором возникло исключение.
  • TargetSite – здесь содержится различная информация о методе в котором возникло исключение – данную информацию можно добавить в лог приложения.

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

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ExceptionHandlingExample
{
    /*
     * обслуживание ошибок при помощи кодов ошибок
     * исключения созданные программистом
     * обслуживание исключений в коде который вызывает выполнение метода
     * обслуживание исключений созданных программистом в коде метода    
     * обслуживание нескольких исключений созданных программистом в коде метода
     * обслуживание системных исключений
     * использование блока finally
     * использование пустого блок catch (с исключением / без исключения)
     * использование блока catch с условием when
     * различные способы выброса исключений
     */

    class Program
    {
        static void Main(string[] args)
        {
            //Исключения основанные на коде ошибок - старый способ
            /*
            var exceptionHandler = new ExceptionHandlerBasedOnCode();

            var resultCode = exceptionHandler.DoSomeWorkException();
            var message = resultCode == 0
                ? "Выполнено без ошибок"
                : $"Выполнено с ошибками: {resultCode}";
            Console.WriteLine(message);
            */

            //Исключения созданные программистом
            /*
            var customExceptionHandler = new ClassWithCustomException();
            customExceptionHandler.DoSomeWorkSeveralExceptionHandling();
          */

            //Системные исключения определенные в среде .Net
            /*
            try
            {
                var systemExceptionHandler = new ClassWithSystemException();
                systemExceptionHandler.DoSomeWorkExceptionWithFinallyOnly();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }*/


            //Различные способы повторного выброса исключений

            try
            {
                var rethrowException = new RethrowingException();
                rethrowException.ThrowNewException();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Вторичный стек вызовов...");
                Console.WriteLine(ex.StackTrace);
                Console.WriteLine("Вторичный стек вызовов - КОНЕЦ.");
                Console.WriteLine();

                if (ex.InnerException != null)
                {
                    Console.WriteLine($"Тип внутреннего исключения: {ex.InnerException.GetType()}");
                    Console.WriteLine("Стек вызовов внутреннего исключения...");
                    Console.WriteLine(ex.InnerException.StackTrace);
                    Console.WriteLine("Стек вызовов внутреннего исключения - КОНЕЦ.");
                }
            }


            Console.ReadKey();
        }
    }

    class ExceptionHandlerBasedOnCode
    {
        public int DoSomeWork()
        {
            for(int i = 0; i < 10;  i++)
            {
                Console.WriteLine("Делаю какую-то работу...");
                Thread.Sleep(500);
            }

            return 0;
        }

        public int DoSomeWorkException()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Делаю какую-то работу...");
                Thread.Sleep(500);
                if (i == 5) return -1;
            }

            return 0;
        }
    }

    class ClassWithCustomException
    {
        public void DoSomeWorkException()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Делаю какую-то работу...");
                Thread.Sleep(500);

                switch (i)
                {
                    case 4:
                        throw new ValueIsFourException("Error! i = 4.");
                    case 5:
                        throw new ValueIsFiveException("Error! i = 5.");
                }
            }
        }

        public void DoSomeWorkExceptionHandling()
        {
            try
            {
                DoSomeWorkException();

            }
            catch (ValueIsFiveException ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.GetType());
            }
        }

        public void DoSomeWorkSeveralExceptionHandling()
        {
            try
            {
                DoSomeWorkException();

            }
            catch (ValueIsFourException ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.GetType());
            }
            catch (ValueIsFiveException ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.GetType());
            }
        }
    }

    class ClassWithSystemException
    {
        public void DoSomeWork()
        {
            var x = 6;
            var y = 0;
            var z = x / y;
        }

        public void DoSomeWork(int x, int y)
        {
            var z = x / y;
        }

        public void DoSomeWorkExceptionHandling()
        { 
            try
            {
                DoSomeWork();
            }
            catch (DivideByZeroException ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.GetType());
            }
        }

        public void DoSomeWorkExceptionHandlingNoExceptionObject()
        {
            try
            {
                DoSomeWork();
            }
            catch (DivideByZeroException)
            {
                Console.WriteLine("ОШИБКА!!!");
            }
        }

        public void DoSomeWorkExceptionHandlingNoExceptionType()
        {
            try
            {
                DoSomeWork();
            }
            catch (Exception) // catch
            {
                Console.WriteLine("ОШИБКА!!! 2222");
            }
        }

        public void DoSomeWorkExceptionWithFinally()
        {
            try
            {
                DoSomeWork();
            }
            catch (DivideByZeroException ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.GetType());
            }
            finally
            {
                Console.WriteLine("Выполняем закрытие файлов, " +
                    "закрываем соединения с базой данных, " +
                    "освобождаем ресурсы");
            }
        }

        public void DoSomeWorkExceptionWithFinallyOnly()
        {
            //данное исключение должно быть обработано выше в стэке вызовов
            //иначе блок finally не будет выполнен.
            try
            {
                DoSomeWork();
            }
            finally
            {
                Console.WriteLine("Выполняем закрытие файлов, " +
                    "закрываем соединения с базой данных, " +
                    "освобождаем ресурсы");

                throw new Exception("TEST");
            }
        }

        public void DoSomeWorkExceptionWithWhen(int x, int y)
        {
            try
            {               
                DoSomeWork(x, y);
            }
            catch (DivideByZeroException ex) when (x == 1)
            {
                Console.WriteLine($"x = {x}");
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.GetType());
            }
        }

        public void DoSomeWorkExceptionHandlingOrder()
        {
            try
            {
                DoSomeWork();
            }           
            catch (DivideByZeroException ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.GetType());
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.GetType());
            }
        }
    }

    class RethrowingException
    {
        public void DoSomeWork()
        {
            var x = 6;
            var y = 0;
            var z = x / y;
        }

        public void Throw()
        {
            try
            {
                DoSomeWork();
            }
            catch (DivideByZeroException ex)
            {
                Console.WriteLine("Первоначальный стек вызовов...");
                Console.WriteLine(ex.StackTrace);
                Console.WriteLine("Первоначальный стек вызовов - КОНЕЦ.");
                Console.WriteLine();
                throw;
            }
        }

        public void ThrowException()
        {
            try
            {
                DoSomeWork();
            }
            catch (DivideByZeroException ex)
            {
                Console.WriteLine("Первоначальный стек вызовов...");
                Console.WriteLine(ex.StackTrace);
                Console.WriteLine("Первоначальный стек вызовов - КОНЕЦ.");
                Console.WriteLine();
                throw ex;
            }
        }

        public void ThrowNewException()
        {
            try
            {
                DoSomeWork();
            }
            catch (DivideByZeroException ex)
            {
                Console.WriteLine("Первоначальный стек вызовов...");
                Console.WriteLine(ex.StackTrace);
                Console.WriteLine("Первоначальный стек вызовов - КОНЕЦ.");
                Console.WriteLine();
                throw new ArgumentNullException("Новое исключение", ex);
            }
        }
    }

    class ValueIsFiveException : ApplicationException
    {
        public ValueIsFiveException(string message) : base(message)
        {
        }
    }

    class ValueIsFourException : ApplicationException
    {
        public ValueIsFourException(string message) : base(message)
        {
        }
    }
}