[Программирование, C#] Лучшие практики обработки исключений в C# (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
13-Апр-2021 19:32
В рамках скорого старта курса "C# Developer. Professional" подготовили для вас перевод материала.
Приглашаем также всех желающих на бесплатный демо-урок «DI-контейнеры для C#». На этом занятии мы:
1) Разберемся с тем, что такое принцип DI и зачем он нужен;
2) Научимся применять DI без использования контейнеров;
3) Рассмотрим два популярных DI-контейнеры для C#: Windsor и Autofac, разберем их плюсы и минусы;
4) Научимся регистрировать зависимости, управлять их жизненным циклом, применять инъекцию зависимостей.
Я плавно приближаюсь к своему двадцатилетнему юбилею в технической индустрии. На протяжении этих лет я своими глазами повидал почти все анти-паттерны обработки исключений (да что уж там, и я сам тоже совершал ошибки). В этой статье я собрал собственные лучшие практики работы с исключениями в C#.Не генерируйте исключения повторноЯ натыкаюсь на это снова и снова. Люди оказываются сбиты с толку тем, что исходный стек трейс «волшебным образом» исчезает при обработке ошибок. Чаще всего это вызвано повторной генерацией исключений. Давайте посмотрим на пример, в котором у нас есть вложенные try/catch:
try
{
    try
    {
        // Вызов какого-либо кода, который может сгенерировать исключение SpecificException
    }
    catch (SpecificException specificException)
    {
        log.LogError(specificException, "Specific error");
    }
    
    // Вызов какого-либо кода
}
catch (Exception exception)
{
    log.LogError(exception, "General erro");
}
Как вы, наверное, уже догадались, внутренний try/catch перехватывает, регистрирует и проглатывает исключение. Чтобы пробросить SpecificException в глобальный блок catch для его обработки, вам нужно пробросить его в стек. Вы можете сделать следующее:
catch (SpecificException specificException)
{
    // ...
    throw specificException;
}
Или так:
catch (SpecificException specificException)
{
    // ...
    throw;
}
Основное отличие здесь состоит в том, что в первом примере повторно генерируется SpecificException, что приводит к сбросу стек трейса исходного исключения, в то время как второй пример сохраняют все детали исходного исключения. Почти всегда предпочтительнее использовать второй пример.Декорируйте исключенияЯ достаточно редко вижу реализацию этой рекомендации на практике. Все исключения расширяют Exception, в котором есть словарь Data. Словарь можно использовать для включения дополнительной информации об ошибке. Отображается ли эта информация в вашем логе, зависит от того, какой фреймворк логирования и хранилище вы используете. В elmah.io записи Data отображаются на вкладке Data.Информацию в словарь Data вносится посредством добавьте пар ключ/значение:
var exception = new Exception("En error happened");
exception.Data.Add("user", Thread.CurrentPrincipal.Identity.Name);
throw exception;
В этом примере я добавляю ключ с именем user с потенциальным именем пользователя, хранящимся в потоке.Вы также можете декорировать исключения, сгенерированные сторонним кодом. Добавьте try/catch:
try
{
    service.SomeCall();
}
catch (Exception e)
{
    e.Data.Add("user", Thread.CurrentPrincipal.Identity.Name);
    throw;
}
Код перехватывает любые исключения, генерируемые методом SomeCall, и добавляет в них имя пользователя. Посредством добавления ключевого слова throw в блок catch исходное исключение пробрасывается дальше по стеку.Перехватывайте в первую очередь наиболее специфические исключенияВероятнее всего, у вас есть где-то код, похожий на этот:
try
{
    File.WriteAllText(path, contents);
}
catch (Exception e)
{
    logger.Error(e);
}
Простой перехват Exception и логирование его в предпочитаемом фреймворке быстро реализуются и справляются со своей задачей. Большинство библиотек, доступных в .NET, могут генерировать ряд различных исключений, и у вас может даже уже быть похожий шаблон в вашей кодовой базе. Перехват нескольких исключений в диапазоне от наиболее до наименее специфической ошибки — отличный способ определить, как вы хотите обрабатывать каждый конкретный тип исключения.В следующем примере я четко демонстрирую понимание, какие исключения следует ожидать и как поступать с каждым конкретным типом:
try
{
    File.WriteAllText(path, contents);
}
catch (ArgumentException ae)
{
    Message.Show("Invalid path");
}
catch (DirectoryNotFoundException dnfe)
{
    Message.Show("Directory not found");
}
catch (Exception e)
{
    var supportId = Guid.NewGuid();
    e.Data.Add("Support id", supportId);
    logger.Error(e);
    Message.Show($"Please contact support with id: {supportId}");
}
Перехватывая ArgumentException и DirectoryNotFoundException перед перехватом общего Exception, я могу показать пользователю специализированное сообщение. В этих сценариях я не регистрирую исключение, поскольку пользователь может быстро исправить ошибки. В случае Exception я генерирую support id, регистрирую ошибку (используя декораторы, как показано в предыдущем разделе) и показываю сообщение пользователю.Обратите внимание, что, хотя приведенный выше код служит для объяснения порядка обработки исключений, реализация потока управления, используя исключения подобным образом — практика не очень хорошая. Это прекрасная подводка к следующему совету:Старайтесь избегать исключенийМожет показаться очевидным, что нужно избегать исключений. Но многих методов, генерирующих исключение, можно избежать с помощью защитного программирования.Одно из самых распространенных исключений — NullReferenceException. В некоторых случаях вы можете разрешить null, но забыть проверить на null. Вот пример, который генерирует NullReferenceException:
Address a = null;
var city = a.City;
Доступ к a выбрасывает исключение. Хорошо, но представьте, что a предоставляется в качестве параметра.Если вы хотите разрешить city с нулевым значением, вы можете избежать исключения, используя null-condition оператор:
Address a = null;
var city = a?.City;
Добавляя ? при доступе к a C# автоматически обрабатывает сценарий, в котором адрес равен null. В этом случае переменной city будет присвоено значение null.Другой распространенный пример исключений — это анализ чисел или логических значений. В следующем примере будет сгенерировано FormatException:
var i = int.Parse("invalid");
Строка invalid не может быть распаршена в виде целого числа. Чтобы не оборачивать это в try/catch, int предоставляет интересный метод, который вы, вероятно, уже использовали 1000 раз:
if (int.TryParse("invalid", out int i))
{
}
В случае, если invalid может быть распаршена как int, TryParse возвращает true и помещает распаршенное значение в переменную i. Еще одно исключение удалось избежать.Создавайте пользовательские исключенияЗабавно вспоминать, как я был Java-программистом (когда .NET находился в стадии бета-тестирования). Мы создавали собственные пользовательские исключения для всего чего угодно. Возможно, это происходило из-за более явной реализации исключений в Java, но я не вижу этого в .NET и C#. Создавая пользовательское исключение, у вас гораздо больше возможностей для перехвата определенных исключений, как уже было показано. Вы можете декорировать свое исключение пользовательскими переменными, не беспокоясь о том, поддерживает ли ваш логгер словарь Data:
public class MyVerySpecializedException : Exception
{
    public MyVerySpecializedException() : base() {}
    public MyVerySpecializedException(string message) : base(message) {}
    public MyVerySpecializedException(string message, Exception inner) : base(message, inner) {}
    public int Status { get; set; }
}
Класс MyVerySpecializedException (возможно, это не то имя класса, которое вы должны использовать в качестве примера :D) реализует три конструктора, которые должен иметь каждый класс исключения. Кроме того, я добавил свойство Status в качестве примера дополнительных данных. Это позволит нам написать такой код:
try
{
    service.SomeCall();
}
catch (MyVerySpecializedException e) when (e.Status == 500)
{
    // Do something specific for Status 500
}
catch (MyVerySpecializedException ex)
{
    // Do something general
}
Используя ключевое слово when, я могу перехватить MyVerySpecializedException, когда значение свойства Status равно 500. Все остальные сценарии попадут в общий catch MyVerySpecializedException.Логируйте исключенияЭто кажется таким очевидным. Но я видел слишком много ошибок в коде в следующих строках при использовании этого шаблона:
try
{
    service.SomeCall();
}
catch
{
    // Игнорируется
}
Логирование как неперехваченных, так и перехваченных исключений — это меньшее, что вы можете сделать для своих пользователей. Нет ничего хуже, чем когда пользователи обращаются в вашу службу поддержки, и вы даже не подозреваете, какие были ошибки и что произошло. В этом вам поможет ведение логов.Существует несколько отличных фреймворков для ведения логов, таких как NLog и Serilog. Если вы веб-разработчик ASP.NET (Core), запись неперехваченных исключений может выполняться автоматически с помощью elmah.io или одного из других доступных инструментов.
Узнать подробнее о курсе "C# Developer. Professional".Смотреть вебинар «DI-контейнеры для C#».

===========
Источник:
habr.com
===========

===========
Автор оригинала: Thomas Ardal
===========
Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_c#, #_csharp, #_windsor, #_autofac, #_di, #_obrabotka_iskljuchenij (обработка исключений), #_blog_kompanii_otus (
Блог компании OTUS
)
, #_programmirovanie (
Программирование
)
, #_c#
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 19-Май 17:40
Часовой пояс: UTC + 5