Довольно много плохих вопросов, которые я вижу на StackOverflow, можно описать следующей формулой:
Вот моё решение домашнего задания. Оно не работает.
[20 строк кода]
И… всё.
Прим. пер.: это перевод статьи "How to debug small programs", на которую ссылаются в справочном разделе английского StackOverflow, посвящённом созданию минимальных, самодостаточных и воспроизводимых примеров. Мне кажется, она прекрасно описывает то, что должен знать каждый программист — основы отладки нерабочего кода.
Если вы читаете эту заметку, то, скорее всего, вы перешли по ссылке, которую либо я, либо кто-то ещё оставил под вашим вопросом на StackOverflow незадолго до того, как этот вопрос был закрыт и удалён. (Если вы читаете эту заметку по другому поводу, оставляйте свои любимые советы по отладке маленьких программ в комментариях).
StackOverflow — это сайт вопросов и ответов, но для очень конкретных вопросов про конкретный код. «Я написал какой-то баганутый код, который не могу исправить» — это не вопрос, это история, причём не слишком интересная. «Почему при вычитании единицы из нуля я получаю число, большее нуля, из-за чего моё сравнение с нулём в строчке 12 некорректно вычисляется в true
?» — вот конкретный вопрос про конкретный код.
Итак, вы просите интернет починить вашу сломанную программу. Наверняка вам никогда не рассказывали, как отлаживать маленькие программы, потому что — внимание — то, что вы сейчас делаете — не самый эффективный способ. Сегодня отличный день, чтобы научиться отлаживать код самостоятельно, потому что StackOverflow не будет этим заниматься вместо вас.
Ниже я предполагаю, что ваша программа компилируется, но работает некорректно, и, более того, у вас есть пример, который демонстрирует это некорректное поведение. Итак, как найти баг:
Для начала включите все предупреждения компилятора. Нет ни одной причины, по которой корректная программа в 20 строк кода могла бы получить предупреждение компилятора. Предупреждение — это когда компилятор говорит вам: «это программа компилируется, но делает не то, что вы думаете», и поскольку это именно та ситуация, в которой вы находитесь, вам следует к нему прислушаться.
Прочитайте предупреждения очень внимательно. Если вы не понимаете, почему было выдано то или иное предупреждение — это хороший вопрос для StackOverflow, потому что это конкретный вопрос о конкретном коде. Добавьте в вопрос точный текст предупреждения, точный код, на котором оно было выдано, и точную версию компилятора, который вы используете.
Если в вашей программе всё ещё есть баг, найдите резиновую уточку. Если резиновой уточки рядом не оказалось, найдите другого студента-программиста — между ним и уточкой нет почти никакой разницы. Объясните уточке простыми словами, почему каждая строчка каждого метода в вашей программе очевидно верна. В какой-то момент вы столкнётесь с трудностями: либо вы не понимаете метод, который написали, либо в нём ошибка, либо и то, и другое. Сконцентрируйтесь на этом методе; скорее всего, проблема именно в нём. Нет, правда, метод утёнка работает. И, как легендарный программист Рэймонд Чен добавил в комментариях ниже (прим.пер.: к исходному посту): если вы не можете объяснить уточке, зачем нужна та или иная строчка — может, это потому что вы начали писать код до того, как у вас появился план.
Если ваша программа компилируется без предупреждений, и у уточки не возникло к вам серьёзных вопросов, и при этом баг всё ещё есть, попробуйте разбить код на методы поменьше, каждый из которых выполняет ровно одну логическую операцию. Популярная ошибка всех программистов, не только начинающих — это писать методы, которые делают несколько вещей одновременно, и делают их плохо. Небольшие методы проще понять, и, как следствие, вам и уточке проще заметить ошибки.
Пока вы переделываете методы в методы поменьше, остановитесь на минутку, чтобы написать техническую спецификацию к каждому методу. Пусть это будет одно или два предложения, но наличие спецификации помогает. Техническая спецификация должна описывать то, что делает метод, какие параметры допустимы, какое ожидается возвращаемое значение, в каких случаях произойдёт ошибка, и так далее. Часто при написании спецификации вы понимаете, что вы забыли обработать какой-то случай, и баг оказывается ровно в этом.
Если у вас всё ещё есть баг, проверьте и перепроверьте, что ваши спецификации описывают предусловия и постусловия каждого метода. Предусловие — это нечто, что должно выполняться перед тем, как метод начал работу. Постусловие — это нечто, что будет выполняться, когда метод закончит работу. Примеры предусловий: «аргумент — корректный ненулевой указатель», «в переданном связном списке хотя бы две вершины» или «этот аргумент — положительное целое число». Примеры постусловий: «в связном списке стало на один элемент меньше, чем было на входе», «определённый кусок массива теперь отсортирован» или что-то в том же духе. Если нарушено предусловие — это ошибка в месте, где метод был вызван. Если при выполнении всех предусловий нарушено постусловие — это ошибка в методе. Опять же, при формулировании пред- и постусловий вы частенько наткнётесь на забытый случай.
Если баг всё ещё никуда не делся, научитесь писать утверждения (assertions), чтобы проверить пред- и постусловия. Утверждение — это почти как комментарий, только оно сообщает вам, когда условие нарушается; а нарушенное условие — это почти всегда баг. В C# вы можете сказать using System.Diagnostics;
в начале кода, а в середине написать Debug.Assert(value != null);
или что-то аналогичное. В каждом языке есть механизм для формулирования утверждений; попросите кого-нибудь рассказать вам, как их использовать в том языке, на котором пишете. Напишите утверждения для предусловий в начале метода, а утверждения для постусловий — перед строчкой, в которой метод завершается. (Конечно, это проще всего сделать, когда в каждом методе есть ровно одна точка возврата.) Теперь, если вы запустите программу и какое-то утверждение окажется неверным, вы узнаете, в чём проблема, и это будет несложно отладить.
После этого напишите тесты или примеры для каждого метода, которые проверяют, что он работает корректно. Тестируйте каждую часть отдельно от остальных, пока вы не будете в ней уверены. Используйте много простых тестов; если ваш метод сортирует списки, попробуйте пустой список, список из одного, двух, трёх одинаковых элементов, три элемента в обратном порядке, несколько длинных списков. Скорее всего, ваш баг проявится на простом тесте, и тогда его будет проще анализировать.
Наконец, если в вашей программе всё ещё есть баг, напишите на бумажке точное описание действия, которое вы ожидаете от каждой строчки кода при запуске на вашем примере. Ваша программа занимает всего 20 строк. У вас должно получиться написать вообще всё, что она делает. Теперь пройдитесь по коду при помощи отладчика, проверяя каждую переменную на каждом шаге и строчка-за-строчкой сверяя поведение вашей программы с бумажкой. Если программа делает то, что не написано на листочке, то ошибка либо на листочке (в этом случае вы не понимаете, что делает ваша программа), либо в программе (в этом случае вы написали что-то неправильно). Поправьте то, что неправильно. Если вы не знаете, как это исправить — у вас как минимум появился конкретный технический вопрос, который вы можете задать на StackOverflow! В любом случае повторяйте процесс до тех пор, пока ваше описание поведения программы и реальное поведение программы не совпадут.
Пока вы работаете в отладчике, я призываю вас обращать внимание на малейшие сомнения. Большинство программистов естественным образом полагают, что их код работает, как ожидается, но ведь вы отлаживаете его ровно потому, что это не так! Много раз я отлаживал код и краем глаза замечал быстрое мелькание в Visual Studio, которое означало «эта память только что была изменена», а я знал, что эта память вообще никак к моей проблеме не относится. Так почему она изменилась? Не игнорируйте эти придирки, изучайте странное поведение до тех пор, пока вы не поймёте, почему оно либо корректное, либо некорректное.
Если вам кажется, что это всё очень долго и требует больших усилий, то лишь потому, что так и есть. Если вы не можете использовать эти техники на программе из двадцати строчек, которую сами же и написали, вряд ли у вас получится использовать их на программе из двух миллионов строчек, которую написал кто-то другой. Тем не менее, разработчики в индустрии занимаются этим каждый день. Начните тренироваться!
И когда вы в следующий раз будете решать домашнее задание, напишите спецификации, примеры, тесты, предусловия, постусловия и утверждения для каждого метода перед тем, как писать сам метод! Вы значительно снизите вероятность появления бага, а если он всё-таки появится — вы, скорее всего, сможете найти его довольно быстро.
Эта методика не поможет найти все баги в каждой программе, но она крайне эффективна для тех коротких программ, которые начинающие программисты пишут в домашних заданиях. Также она может быть использована и для нахождения багов в нетривиальных программах.
Автор: Егор Суворов