В рамках скорого старта курса "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#».
