Введение

Сколько раз вы сталкивались с перекрестным «NullReferenceException» в производственной среде и тратили долгие часы, чтобы выяснить, в чем причина проблемы? При этом встречали ли вы одно из следующих утверждений?

return null; 

or

return 0;

or

return -1;

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

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

Задний план

Первый вопрос: почему возникает исключение? Программное обеспечение имеет несколько функций и подфункций. Каждая функция выполняется последовательно и может иметь входные и выходные аргументы. Как правило, выход одной функции становится входом другой (рис. 1-а). Пока мы пишем функцию, мы имеем дело с входными и выходными переменными. Мы делаем некоторые предположения об этих переменных, сознательно или бессознательно. Наш код работает хорошо, если наши предположения работают (рис. 1-а). Если происходит что-то, противоречащее нашим предположениям, наш код ведет себя неожиданно (рис. 1-b). Чтобы предотвратить такое неожиданное поведение, мы обычно генерируем исключения. Таким образом, хорошее управление исключениями начинается с правильных предположений.

Рис. 1

Как делать предположения?

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

Customer GetCustomerByEmail(String email);
Customer GetCustomerById(int customerId);

И мы используем одну из этих функций:

Customer customer =  customerProvider.GetCustomerByEmail(email);
//Should I check null value?
CallCustomer(customer.PhoneNumber);
TNullable<Customer> GetCustomerByEmail(String email);
TNullable<Customer> customer =  customerProvider.GetCustomerByEmail(email);
if (customer.HasValue()){
    CallCustomer(customer.Value.PhoneNumber)
}

Обратите внимание, что тип Nullable<t> нельзя использовать со ссылочными типами. Поэтому я написал тип TNullable<t>, который можно использовать со ссылочными типами. К счастью, ссылочные типы, допускающие значение NULL, были представлены в C# 8. Вам не нужно создавать новый тип, если вы используете C# 8.

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

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

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

//A SaleOrder has always an existing customer.
Customer customer = customerProvider.GetCustomerById(order.CustomerId);

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

public Customer GetCustomerById(int customerId){
	Customer customer  = db.GetCustomerById(customerId);
	if (customer == null)
		throw new EntityNotFoundException("Customer not found. CustomerId:" + customerId);
	return customer;
}

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

TNullable<Customer> customer = db.GetCustomerByEmail(txtEmail.Text);

Мы не можем предположить, что клиент с данным адресом электронной почты всегда существует в базе данных. Вполне возможно, что какой-либо клиент с указанным адресом электронной почты не существует. В таких случаях вместо возврата типа клиента лучше вернуть тип Customer (TNullable<Customer>), допускающий значение NULL, чтобы сообщить разработчику, что этот метод может возвращать нулевое значение.

public TNullable<Customer> GetCustomerByEmail(String email){
    Customer customer  = db.GetCustomerByEmail(email);
    return new TNullable<Customer>(customer);
}

Когда обрабатывать исключения?

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

О примерах

Скачать пример

Для демонстрации идеи, о которой я говорил, я создал пример приложения. Этот пример приложения содержит очень простые классы моделей, которые содержат всего несколько свойств, которые мы собираемся использовать.

Другие файлы/классы в проекте:

  • DBConnection: фиктивный класс базы данных, который возвращает предопределенные объекты.
  • TNullable: класс для определения ссылочных типов, допускающих значение Nullable.
  • EntityNotFoundException: собственный класс исключений
  • Example1, Example2, Example3: Три примера файлов. Подробная информация ниже
  • Program: Точка входа приложения. По умолчанию метод Main выглядит примерно так:
static void Main(string[] args)
{
	try
	{
		RunExample1(); 
		//RunExample2();
		//RunExample3();                 
	}
	catch (Exception ex)
	{
		LogEx(ex);
	}
	Console.ReadLine();
}

Вы можете запустить другие примеры. Просто закомментируйте RunExample1() и раскомментируйте одну из других строк.

Пример1

Исходный код с плохой практикой. На самом деле никакого управления исключениями нет. Когда вы бежите, вы получаете «NullReferenceException». Вы понятия не имеете, что вызывает это исключение.

Пример2

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

Пример3

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

Вывод

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