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

четверг, 3 июня 2010, Oleksandr Reminnyi

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

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

Рассмотрим такие ситуации, когда вызываемая подпрограмма выполняет следующие операции:

  1.     Вычисление корня отрицательного числа;
  2.     Вычисление атрибутов файла на диске, которого не существует;
  3.     Выход за границы массива.

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

  1.     Бросаем ArgumentException
  2.     Возвращаем -1, null, идентифицируем результат как некорректный и т.п.

Итак, вычисление корня отрицательного числа

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

В С# функция Math.Sqrt принимает на вход значения типа double. Этот тип данных предполагает как положительные так и отрицательные значения. То есть если функция может получить отрицательное значение, значит этот вариант уже не являеться абсолютно исключительным, и следовательно – генерация исключения может и не происходить.

double sqrt = Math.Sqrt(-4); // sqrt == double.NaN
double sum = sqrt + 2; // sum == double.NaN

Если сделать проверку то получим значение NaN, что обозначает Not A Number –double константа как для Java так и для C#. Тоесть .NET проидентифицировал результат как некорректный, но при этом не прервал выполнения. С этим числом возможны дальнейшие арифметические операции, но уже каждая из них будет возвращать NaN как результат. То есть машинное время будет съедаться программой для продолжения выполнения некорректных рассчетов, что не совсем логично. Почему так – смотрите Пояснение в конце статьи.

Если же мы определим собственную функцию Math1.Sqrt, которая будет принимать на вход только uint (так определим только положительные числа) значения, то передать отрицательное значение внутрь такой конструкции вы не сможете изза ошибки на этапе компиляции.

Вычисление атрибутов файла на диске, которого не существует

Аналогично первым делом на ум приходит ArgumentException. Файла не существует, но как ни странно, следующая строка кода не генерирует исключение:

DateTime dt = File.GetLastAccessTime("C:\\I_warn_you_there_is_no_such_file.txt");

Хм… Строка корректна, имя файла тоже. И возвращаемый результат – также корректен (относительно). Это дата: 12:00 полночь, 1 Января, 1601 н.э.То есть в принципе, программа может продолжать работать, но не факт что она не свалиться на следующем шаге. Значит ситуация, когда на вход функции передается несуществующий файл – не является исключительной.

Но вот примечательный момент – следующая строка сгенерирует ArgumentException исключение:

DateTime dt = File.GetLastAccessTime("");

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

Выход за границы массива

Следующий код имеет нечто общее с предыдущими случаями.

int[] array = new[] { 10, 12, 13 };
int val = array[4];

Индекс 4 также является просто неправильным параметром. И, возможно, есть смысл вернуть default для данного элемента массива? Почему бросается исключение (IndexOutOfRangeException)?

Пояснение

Эндрю Хант и Дэвид Томас, авторы книги «Программист - прагматик», советуют внутри подпрограммы генерировать исключение только в том случае, если ситуация является частью контекста подпрограммы. Это хорошо объясняться на последнем примере, когда в случае с обращением к несуществующему элементу массива мы непосредственно находимся в контексте существования данного массива. Или же в случае, если мы вычисляем корень – и алгоритм не срабатывает корректно, или результат выходит за границы погрешности. В других же случаях, когда в подпрограму передается некорректное значение, его контролем должна заниматься вызывающая программа. То есть перед тем как передать в Math.Sqrt негативное значение, сначала вызывающая программа должна проверить его и правильно обработать. Например, сгенерировать то же исключение.

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

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

Parameter

Return value

Zero, or positive

The positive square root of d.

Negative

NaN

Equals NaN

NaN

Equals PositiveInfinity

PositiveInfinity

P.S. Спасибо коллегам Сергею Е., Виктору П., Сергею З., благодаря дискуссии с которыми и появился данный очерк.

Компании из статьи


Microsoft Украина


Сайт:
http://www.microsoft.com/ukr/ua/

Microsoft Украина Украинское подразделение компании Microsoft.

Ищите нас в интернетах!

Комментарии

Свежие вакансии