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

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

Для начала давайте выясним, а что же это за зверь такой. Я выделяю следующие свойства:
  • невозможно игнорировать наступление;
  • несколько обработчиков в одном месте;
  • просачивание через любое количество вложенных вызовов;
  • независимая, передаваемая обработчику, структура данных — вспомните hresult, макросы вызова com-фунций и другую белиберду, обязанную своим существованием отсутствию (или нежеланию использовать) механизмов exception-ов

И все. Всевозможные потери в производительности, сложность контроля являются контекстно-зависимыми (а чаще просто надуманными) и требуют доказательства в каждом конкретном случае.

Например: производим поиск по файлу.

int FindSymbol(TextReader reader, char symb)
{
  char cur;
  int pos;
  while (cur = (char)reader.Read())
  {
    if (cur == ‘a’)
      throw new FormatException();
    pos ++;
    if (cur == symb)
      return pos;
  }
  throw new MyException(); }
}



Определены следующие варианты выхода из функции:
  • требуемые данные найдены (return pos)
  • файл имеет не правильный формат (throw)
  • файл закончился (reader поднимает исключение)

Последний вариант работы функции наиболее спорен. Обычно программисты очень изобретательны при обосновании своих решений, и могут найти кучу доводов «за» и «против»: неэффективность, клиентскому коду необходимо знать детали реализации, неправильный формат является ошибкой и т.д. Все это правда, но не главное. Основной довод «за» — достаточность.

Клиенту больше не надо анализировать сложный результат, он попросил найти текст — ему нашли; если нет — он об этом даже не узнает.

Еще пример: обработка нажатия пользователем кнопки «cancel». Опять же можно долго дискутировать на тему — для чего изначально создавался этот механизм?

Но если он есть и подходит для логики «cancel» — глупо его не использовать.
Когда мне нужно было подклеить дома порожек, я, недолго думая, использовал в качестве груза книгу М.Мак-Дональда «Wpf …». Так почему же в иных случаях мне поступать иначе?

Как вы относитесь к маленьким функциям? Я вот считаю, что другие просто не имею�� права на существование. При таком отношении периодически возникают ситуации, что без поднятия исключений просто никуда. Например, надо выйти из цикла, расположенного выше по стеку вызовов. Не передавать же флаг завершения, в конце концов. Или передать…
Если же цикл располагается не на предыдущем уровне стека вызовов, а выше? Весь код без throw превращается в анализ результатов работы функций. Доходит до смешного — выделяются методы проверяющие результаты работы других функций. Сидишь и думаешь об откате неожиданно «неправильного» рефакторинга. Но все меняется, когда приходят они: код начинает удаляться по две-три строчки за раз (а если я тормозил
до этого достаточно долго, то удаляются и методы, и классы)

А вот еще один интересный «аромат» — большое количество условных операторов. Когда перед выполнением какого-либо действия начинается анализ, «а можно ли это сделать». Оставим в покое алгоритмы, состоящие из нескольких операций, которые по отдельности делают состояние системы неопределенным. Если начал делать, — разбейся в лепешку, но закончи! Я не про них. Хотя и здесь, при желании, можно найти приемлемые решения. Так вот, запах от большего количества условных операторов: ну например, при анализе набора входных данных перед выполнением операции или проверка валидности структуры данных, или… ну мало ли чего еще. В общем, смотрим на каждый элемент данных и решаем, а подходит ли он для наших великих целей, а затем осуществляем их. Не глупо ли? Можно же сразу попробовать осуществить задуманное. И всего-то надо — предположить, что на данном участке может произойти исключение, связанное с нашим нежеланием загромождать код мусором.

Основным аргументом у противников такого отношения к подъему исключений является просадка производительности. Глупо с этим спорить — условный оператор в несколько раз более эффективен. Но есть одно «но»: проблему надо доказать на реальных данных. Предположим, участок кода действительно критический. О ужас! — теперь придется потратить пару часов на рефакторинг. Не дней, а именно часов. Помните как рассказывал Фаулер о своем отношении к проектированию: «…я пытаюсь определить, насколько трудно окажется рефакторинг от одного дизайна в другой. Если трудностей не видно, то я, не слишком задумываясь о выборе, останавливаюсь на самом простом».

Если предположить, что мы принимаем эту смелую мысль, то осталось определиться с порядком рефакторинга:
  1. Выделяем метод из тела метода поднимающего исключение.
  2. Определяем критерий в сигнатуре выделенного метода, который будет показывать, что в иной ситуации мы бы подняли исключение.
  3. Изменяем исходный метод таким образом, чтобы он, в зависимости от критерия, поднимал исключение. Будем называть исходный метод «методом с исключением», а выделенный — «метод без исключения».
  4. Создаем метод расширение (копированием) из метода без исключения.
  5. Заменяем тело метода расширения на вызов метода с исключением.
  6. Блокируем распространение исключения через метод расширения и, в зависимости от подъема, взводим или сбрасываем критерий.
  7. Добавляем к интерфейсу метод с сигнатурой метода б��з исключения. В реализациях этих методов производим вызов метода расширения. Если метод не входит в состав внешнего интерфейса (по отношению к классу) — тем лучше. Этот пункт можно пропустить.
  8. Заменяем вызовы метода с исключениями методами без исключения. В реализациях интерфейса это обязательно (иначе не следовало и начинать), в остальных случаях — по желанию. Замещающий код должен выглядеть так: вызов метода без исключения, анализ критерия.
  9. Создаем метод расширения из метода с исключением. Структура его должна уже быть верной. Осталось только сделать его рабочим.
  10. Заменяем оставшиеся вызововы с исключениями на расширения без исключения.
  11. Удаляем из интерфейса методы с исключениями и расширения без исключения. Расширения с исключениями по возможности.


Вот как это будет выглядеть в коде.
  • Исходный код:
    class SrcClass
    {
      public void Exec()
      {
        throw new MyException();
      }
    }
  • После третьего шага:
    class SrcClass
    {
      public void Exec()
      {
        if (!TryExec())
          throw new MyException();
      }
      public bool TryExec()
      {
       return false;
      }
    }
  • После девятого шага:
    class SrcClass
    {
      public void Exec()
      {
        if (!TryExec())
          throw new MyException();
      }
      public bool TryExec()
      {
        return false;
      }
    }

    static class SrcClassHelper
    {
      public static bool TryExecHelp(this SrcClass src)
      {
        try
        {
          src.Exec();
          return true;
        }
        catch (MyException)
        {
          return false;
        }
      }
      public static void Exec(this SrcClass src)
      {
        if (!src.TryExec())
          throw new MyException();
      }
    }
  • По завершению:
    class SrcClass
    {
      public bool TryExec()
      {
        return false;
      }
    }

    static class SrcClassHelper
    {
      public static void Exec(this SrcClass src)
      {
        if (!src.TryExec())
          throw new MyException();
      }
    }


В принципе все. Надеюсь, я сумел развеять хотя бы часть страхов о подъеме исключений, и теперь при возникновении такой необходимости вы более лояльно будете выбирать решение. И скорее всего, выберите более простое. А значит, ваш приемник будет ругать вас с меньшим остервенением (не исключено что им буду я).