При работе над большими проектами, становится сложно отследить место возникновения той или иной ошибки. Особенно, если эта ошибка не приводит к созданию исключения, а просто негативно влияет на конечный результат.
Можно ждать пока ошибка возникнет и производить отладку шаг за шагом чтобы найти её, а можно предусмотреть возникновение ошибки заранее и быть наготове.
Вообще, ошибки можно разделить на два типа:
- Ошибки во внешних данных
- Ошибки в логике приложения
Оглавление
Ошибки во внешних данных
К первому типу относятся в том числе ошибки:
- В данных введённых пользователем, считанных из файла или из консоли;
- Ошибки связанные с отсутствием файлов, невозможности их прочитать или записать;
- “Битые данные” принятые по сети;
- Ошибки, поизошедшие во внешних устройствах;
- В данных, принятых из других модулей большого проекта.
Чаще всего, ошибки этого типа мы можем предугадать, отловить и обработать.
Один из вариантов обработки таких ошибок заключается в создании исключений. (об этом будет сказано ниже)
Приложение при этом может поступить одним (или несколькими) из приведённых ниже способов:
- Мягко завершиться (без потери данных и непонятных пользователю сообщений);
- Оповестить пользователя о возникновении ошибки (внятным сообщением, позволяющим понять, что нужно делать);
- Заменить некорректные данные приближёнными к истине или просто корректными значениями;
- Повторить попытку получить данные;
- Пропустить выполнение методов, зависящих от некорректных данных.
Исключения
Если параметрами функции служат значения, которые были введены пользователем или взяты из файла, то может возникнуть такая ситуация, при которой введённые данные не могут быть корректно обработаны.
Если метод, в котором возникла такая ситуация, не должен иметь полномочия на решение этой проблемы (например, если это нарушит абстракцию, или поведение программы должно различаться в зависимости от того, кто вызвал этот метод), то мы можем передать информацию о возникшей ошибке вызвавшему методу, сгенерировав исключение.
1 2 3 4 5 6 7 8 |
class MinusDeltaException { }; int CrashingMethod(int lastTime, int delta) throw (MinusDeltaException) { if (delta < 0) throw MinusDeltaException(); return lastTime + delta; } |
Мы добавили собственное исключение MinusDeltaException, которое будет создано и отправлено вызывающему методу при передаче нашей функции отрицательного второго параметра. При возникновении такого исключения строка return lastTime + delta; выполнена не будет.
В C++ описания вида throw (MinusDeltaException) в заголовке функции писать не обязательно, но желательно (особенно для больших методов). Так Вы никогда не забудете, что функция генерирует эти исключения.
Теперь, пора обработать наше исключение.
1 2 3 4 5 6 7 8 9 10 11 |
void SomeMethod() { try { CrashingMethod(100, -1); } catch(MinusDeltaException) { ShowMessage("Ошибка, дельта времени не может быть отрицательной"); } } |
Сам CrashingMethod производит в данном случае только сложение, а при возникновении ошибки, сбрасывает с себя полномочия по её разрешению на вызывающий метод. Вызывающий метод уже решает что сделать: оповестить пользователя, попробовать считать данные снова, записать ошибку в лог и продолжить и т.д.
Если мы хотим поручить обработку ошибки самой функции, то исключение создавать и обрабатывать не нужно. Исключения создаются для того, чтобы вызывающий метод мог принять решение о том, как поступить при возникновении ошибки. Работа с исключениями напоминает работу с кодами ошибок: мы можем при возникновении ошибки передать в выходном значении код ошибки и завершить выполнение функции, а вызывающий метод будет на основе кода решать что ему делать дальше.
Необработанные исключения лучше не оставлять в коде. Так же, не стоит просто глушить исключение, оставляя блок catch пустым.
В работе с исключениями есть ещё много интересных особенностей, советую не упускать возможности узнать о них побольше.
Ошибки в логике приложения
Ко второму типу ошибок, относятся ошибки программистов. Это всяческие ошибки логики, не инициализированные данные или обращение к несуществующим данным.
Таких ошибок можно избежать только в теории. Ни один программист не пишет код без ошибок. Проекты сейчас слишком необъятны для того, чтобы целиком уложить в сознание, а требования к ним постоянно меняются.
В отличии от ошибок в данных и внешних подсистемах, такие ошибки не нужно обрабатывать (хотя иногда стоит), их стоит отлавливать и искоренять. Для упрощения нахождения ошибок, мы можем использовать утверждения. (об этом ниже)
К примеру, у нас есть функция, которая увеличивает игровое время на некоторое значение, затраченное на какое-либо действие.
1 2 3 4 |
int CrashingMethod(int lastTime, int delta) { return lastTime + delta; } |
Если вдруг второй параметр будет отрицательным, мы получим ситуацию, когда игровое время уменьшается. Из этого вряд ли выйдет что-то хорошее (если только мы не собираемся перемещать игрока в прошлое).
Утверждения
Если наша функция получает значения как результат вычислений других функций, и эти значения никогда не должны выходить из необходимого диапазона, то это ещё не значит что они из него никогда не выйдут.
Для того чтобы отследить ошибки в коде, связанные с возвратом или передачей в параметры ошибочных значений, можно использовать утверждения.
Немного поэкспериментировав я пришёл к такому объявлению утверждений в своих проектах на C++.
UPD (05.10.12):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#define DEBUG #if defined(DEBUG) #define WARN(message) \ { \ cout << "Warning: "; \ cout << message << endl; \ exit(1); \ } #define WARN_IF(condition, message) \ { \ if (condition) \ { \ cout << "Warning "; \ cout << condition; \ cout << " : "; \ cout << message << endl; \ exit(1); \ } \ } #else #define WARN(message) #define WARN_IF(condition, message) #endif |
В приведённом коде, записаны два макроса. Первый из них выводит сообщение об ошибке с заданным текстом в консоль и завершает работу приложения; второй делает то же, но только при выполнении условия condition.
Если определён DEBUG, то препроцессор перед компиляцией обрабатывает и вставляет в код данные макросы вместо WARN_IF и WARN, иначе просто удаляет вызовы.
Другими словами, если закомментировать строку #define DEBUG, то никаких проверок, выводов сообщений и завершений приложения при вызове макросов происходить не будет.
Внимание: Если вставить функцию в проверку WARN_IF, то, после удаления DEBUG, вызов функции в этом месте будет удалён. Будьте внимательны, иначе при уходе с DEBUG могут появиться новые ошибки, связанные с удалением вызовов.
Модифицируя нашу функцию, приходим к следующему виду.
1 2 3 4 5 6 |
int CrashingMethod(int lastTime, int delta) { WARN_IF (delta < 0, "Отрицательная дельта времени в CrashingMethod()") return lastTime + delta; } |
Теперь, если однажды в функцию будет подан отрицательный параметр delta, она запишет сообщение с ошибкой в консоль и прекратит работу. Это не позволит программисту проигнорировать ошибку. В сообщении мы как можно более детально указываем место возникновения ошибки. Лучше всего указать класс и метод, в котором записано это утверждение, а так же кратко описать ошибку.
Макрос WARN предназначен для отслеживания веток кода, которые никогда не должны выполняться.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
void Move(EDirection direction) { switch (direction) { case drUp: if (HeroY != 0) HeroY--; break; case drDown: if (HeroY < MapHeight - 1) HeroY++; break; case drLeft: if (HeroX > 0) HeroX--; break; case drRight: if (HeroX < MapWidth - 1) HeroX++; break; default: WARN("Несуществующее направление в Move()"); } CheckWinOrLose(); Draw(); } |
Тут, если вдруг какой-то метод решит подвинуть героя в пятом направлении, будет показано сообщение об ошибке и приложение завершится.
Если Вы пишите dll для UDK, то наверняка столкнулись с отсутствием возможности отладить их код. Последний два способа позволяют ускорить поиск места возникновения ошибки, а главное позволяют сразу же выявить само наличие ошибки в коде. Ведь бывает, что ошибка находится через недели, месяцы и годы после написания кода.
UScript
В UScript нет системы для работы с исключениями, поэтому приходится довольствоваться кодами ошибок, которые можно передавать как out параметры или возвращать как значение функции.
Как Вы уже наверно знаете, макрос warn уже определён в UScript. Он позволяет найти, с точностью до метода, место где выполнился код, который не должен был выполняться. В отличие от приведённого выше макроса WARN, warn не завершает выполнение приложения после возникновения непредвиденной ситуации.
Замену макросу WARN_IF я не нашёл, так что записал его определение в файле Globals.uci первого компилируемого пакета моего проекта.
1 2 3 4 5 6 |
`if (! `isdefined(FINAL_RELEASE)) `define Warn_if(cond, mess)\ {if (`cond) `warn("<warn_if>"@`mess)} `else `define Warn_if `endif |
А вот и аналог функции, которая может принять неверные значения.
1 2 3 4 5 6 |
function int CrashTest(int lastTime, int delta) { `warn_if(delta < 0, "Otricatelnaya delta vremeni"); return lastTime + delta; } |
При попытке передать в код отрицательную дельту мы получим сообщение примерно следующего содержания:
[0012.76] ScriptWarning: <warn_if> Otricatelnaya delta vremeni
UnPlayerController UN-TestHouse.TheWorld:PersistentLevel.UnPlayerController_0
Function Universe.UnPlayerController:CrashTest:003B
Я написал сообщение транслитом не просто так. Кириллический текст хоть и отображается в консоли, но приходит к нечитаемому виду будучи записанным в файл лога.
Leave a Reply
You must be logged in to post a comment.