3.3 – Стратегия отладки
При отладке программы в большинстве случаев большая часть вашего времени будет потрачена на то, чтобы выяснить, где на самом деле находится ошибка. Как только проблема будет обнаружена, оставшиеся шаги (устранение проблемы и подтверждение того, что проблема была устранена) часто оказываются тривиальными по сравнению предыдущим шагом.
В этом уроке мы начнем изучать, как находить ошибки.
Поиск проблем с помощью проверки кода
Допустим, вы заметили проблему и хотите выяснить ее причину. Во многих случаях (особенно в небольших программах) мы можем быстро определить, где находится проблема.
Рассмотрим следующий фрагмент программы:
int main()
{
getNames(); // просим пользователя ввести несколько имен
sortNames(); // сортируем их в алфавитном порядке
printNames(); // выводим отсортированный список имен
return 0;
}
Если вы ожидали, что эта программа напечатает имена в алфавитном порядке, но вместо этого она напечатала их в обратном порядке, проблема, вероятно, в функции sortNames
. В случаях, когда вы можете сузить проблему до конкретной функции, вы можете обнаружить проблему, просто взглянув на код.
Однако по мере того, как программы становятся более сложными, поиск проблем путем проверки кода также становится более сложным.
Во-первых, необходимо проанализировать много кода. Просмотр каждой строчки кода в программе, состоящей из тысяч строк, может занять очень много времени (не говоря уже о том, что это невероятно скучно). Во-вторых, сам код имеет тенденцию быть более сложным, с большим количеством возможных мест, где что-то может пойти не так. В-третьих, поведение кода может не дать вам много подсказок о том, где что-то идет не так. Если бы вы написали программу для вывода рекомендаций по акциям, а она на самом деле вообще ничего не выводила, у вас, вероятно, не было бы особого представления о том, с чего начать поиск проблемы.
Наконец, ошибки могут быть вызваны неверными предположениями. Ошибку, вызванную неверным предположением, обнаружить визуально практически невозможно, потому что вы, вероятно, сделаете такое же неверное предположение при проверке кода и не заметите ошибку. Итак, если у нас есть проблема, которую мы не можем найти с помощью проверки кода, как ее найти?
Поиск проблем при запуске программы
К счастью, если мы не можем найти проблему с помощью проверки кода, есть еще один путь, которым мы можем воспользоваться: мы можем наблюдать за поведением программы во время ее работы и пытаться диагностировать проблему на основе этого. Этот подход можно обобщить следующим образом:
- выясните, как воспроизвести проблему;
- запустите программу и соберите информацию, чтобы сузить область, где может находиться проблема;
- повторяйте предыдущий шаг, пока не найдете проблему.
В оставшейся части этой главы мы обсудим методы, облегчающие этот подход.
Воспроизведение проблемы
Первый и самый важный шаг в поиске проблемы – это возможность воспроизвести проблему. Причина проста: проблему очень сложно найти, если вы не заметите, как она возникает.
Вернемся к нашей аналогии с автоматом для льда: допустим, однажды ваш друг говорит вам, что ваш автомат для льда не работает. Вы идете посмотреть на него, и он отлично работает. Как бы вы диагностировали проблему? Это было бы очень сложно. Однако если вы действительно заметили, что дозатор льда не работает, вы могли бы начать более эффективно диагностировать, почему он не работает.
Если проблема с программным обеспечением очевидна (например, программа дает сбой в одном и том же месте каждый раз, когда вы ее запускаете), то воспроизведение проблемы может быть тривиальным. Однако иногда воспроизвести проблему может быть намного сложнее. Проблема может возникать только на определенных компьютерах или при определенных обстоятельствах (например, когда пользователь вводит определенные данные). В таких случаях может оказаться полезным создание набора шагов воспроизведения. Шаги воспроизведения – это список четких и точных шагов, которые можно выполнить, чтобы вызвать повторяемость проблемы с высоким уровнем предсказуемости. Цель состоит в том, чтобы вызвать как можно большую повторяемость проблемы, чтобы мы могли запускать нашу программу снова и снова и искать подсказки, чтобы определить, что вызывает проблему. Если проблема может быть воспроизведена в 100% случаев, это идеально, но воспроизводимость менее 100% тоже может быть допустимой. Проблема, которая возникает только в 50% случаев, просто означает, что на диагностику проблемы уйдет в два раза больше времени, поскольку в половине случаев программа не проявит проблему и, следовательно, не предоставит никакой полезной диагностической информации.
Локализация проблемы
Как только мы сможем разумно воспроизвести проблему, следующим шагом будет выяснить, где проблема находится в коде. В зависимости от характера проблемы это может быть легко или сложно. Для примера предположим, что мы не очень хорошо понимаем, в чем на самом деле проблема. Как мы ее находим?
Здесь нам хорошо послужит аналогия. Давайте поиграем в высоко-низко. Я попрошу вас угадать число от 1 до 10. Для каждого вашего предположения я скажу вам, является ли каждое предположение слишком высоким, слишком низким или правильным. Пример этой игры может выглядеть так:
Вы: 5
Я: слишком низко
Вы: 8
Я: слишком высоко
Вы: 6
Я: слишком низко
Вы: 7
Я: Правильно
В приведенной выше игре вам не нужно перебирать все числа, чтобы найти число, которое я придумал. Делая предположения и рассматривая информацию, которую вы извлекаете из каждого предположения, вы можете «локализовать» правильное число, сделав лишь несколько предположений (если вы используете оптимальную стратегию, вы всегда можете угадать число, которое я загадал, за 4 или менее попыток).
Мы можем использовать аналогичный процесс и для отладки программ. В худшем случае мы можем не знать, где находится ошибка. Однако мы знаем, что проблема должна быть где-то в коде, который выполняется между началом программы и моментом, когда программа проявляет первый неверный симптом, который мы можем наблюдать. Это, по крайней мере, исключает те части программы, которые выполняются после первого наблюдаемого симптома. Но это по-прежнему оставляет много кода для анализа. Чтобы диагностировать проблему, мы сделаем несколько обоснованных предположений о том, в чем проблема, чтобы быстро найти ее.
Часто то, что заставило нас заметить проблему, дает нам первоначальное предположение, которое близко к тому, в чем заключается настоящая проблема. Например, если программа не записывает данные в файл, когда должна это делать, то проблема, вероятно, где-то в коде, который обрабатывает запись в файл (да!). Затем мы можем использовать стратегию типа высоко-низко, чтобы попытаться определить, где на самом деле находится проблема.
Например:
- Если мы можем доказать, что в какой-то точке нашей программы проблема еще не возникла, это аналогично получению результата «слишком низко» в высоко-низко – мы знаем, что проблема должна быть в программе где-то позже. Например, если наша программа каждый раз дает сбой в одном и том же месте, и мы можем доказать, что программа аварийно не завершается в определенный момент выполнения, то сбой должен быть в коде позже.
- Если в какой-то момент в нашей программе мы можем наблюдать некорректное поведение, связанное с проблемой, то это аналогично получению результата «слишком высоко» в высоко-низко, и мы знаем, что проблема должна быть в программе где-то раньше. Например, допустим, программа печатает значение некоторой переменной
x
. Вы ожидали, что она напечатает значение 2, но вместо этого напечатала 8. Переменнаяx
должна иметь неправильное значение. Если в какой-то момент во время выполнения нашей программы мы видим, что переменнаяx
уже имеет значение 8, значит, мы знаем, что проблема должна была возникнуть до этого момента.
Аналогия «высоко-низко» не идеальна – мы также можем иногда исключать из рассмотрения целые разделы нашего кода, не получая никакой информации о том, появляется ли реальная проблема до или после этой точки.
В следующем уроке мы покажем примеры всех трех случаев.
В конце концов, обладая достаточным количеством догадок и хорошей методикой, мы можем точно определить строку, вызывающую проблему! Если мы сделали какие-то неверные предположения, это поможет нам выяснить, где именно. Когда вы исключили всё остальное, единственное, что осталось, должно быть причиной проблемы. Тогда нужно просто понять, почему.
Какую стратегию угадывания вы захотите использовать, зависит от вас – лучший вариант зависит от типа ошибки, поэтому, чтобы локализовать проблему, вы, вероятно, захотите попробовать много разных подходов. По мере того, как вы приобретаете опыт в отладке неисправностей, ваша интуиция всё больше будет вам помогать.
Так как же нам «делать предположения»? Для этого есть много способов. В следующей главе мы начнем с некоторых простых подходов, а затем будем развивать их и изучать другие в следующих главах.