Как и какими средствами находить ошибки в коде на C#?
Я занимаюсь разработкой и когда пишу код, то он иногда не работает так, как я задумывал или даже вообще не работает. Сижу и смотрю на него, гадаю: что и где не так?
Не могу усидеть — иду сразу на ресурсы профессионалов, например Stack Overflow и публикую вопрос "Где здесь ошибка?" или "Почему не работает?"
Но оказывается часто, что проблема мелкая: дурацкая опечатка, ошибка в синтаксисе или еще какая мелочь. Профессионалом так не станешь, если по каждой ерунде бегать по ресурсам. А я хочу им быть.
Вопрос: какие есть способы, чтобы найти ошибки в коде на C#? Какие есть инструменты, методы и т.д.?
Вчера всё работало, а сегодня не работает / Код не работает как задумано
Debugging (Отладка)
В чем заключается процесс отладки? Что это такое?
Процесс отладки состоит в том, что мы останавливаем выполнения скрипта в любом месте, смотрим, что находится в переменных, в функциях, анализируем и переходим в другие места; ищем те места, где поведение отклоняется от правильного.
Заметка: Отладка производится как правило в IDE (Интегрированная среда разработки). Что это такое можно чуть подробнее ознакомиться в вопросе
Какие есть способы предупреждения ошибок, их нахождения и устранения?
В данном случае будет рассмотрен пример с Visual Studio, но отладить код можно и в любой другой IDE.
Подготовка
Достаточно иметь в наличии IDE, например Visual Studio
Запуск
Для начала в левой части панели с кодом на любой строке можно кликнуть ЛКМ , тем самым поставив точку останова (breakpoint — брейкпойнт). Это то место, где отладчик автоматически остановит выполнение C#, как только до него дойдёт. Количество breakpoint’ов не ограничено. Можно ставить везде и много.

Отладка запускается сочетанием F5 или выбором в верхнем меню Debug → Start Debugging

В данном случае, т.к. функция вызывается сразу на той же странице, то при нажатии кнопки Debug — отладчик моментально вызовет метод, выполнение "заморозится" на первом же брейкпойнте. В ином случае, для активации требуется исполнить действие, при котором произойдет исполнение нужного участка кода (клик на кнопку в UI, передача POST запроса с данными и прочие другие действия)

- Стэк вызовов, все вложенные вызовы, которые привели к текущему месту кода.
- Переменные. На текущий момент строки ниже номера 8 ещё не выполнилась, поэтому определена лишь data и numsStringArr
- Показывает текущие значения любых переменных и выражений. В любой момент здесь можно вписать имя любой переменной в поле name и посмотреть её значение в реальном времени. Например data или nums[0] , а можно и nums[i] и item.test.data.name[5].info[key[1]] и т.д. На текущий момент строки ниже номера 24 ещё не выполнилась, поэтому num (которая появляется лишь в цикле foreach ) во вкладке Watch обозначена красным цветом с надписью "The name ‘num’ does not exist in the current context".
Процесс
Для самого процесса используются элементы управления (см. изображение выше, выделено зеленым прямоугольником)

Show Next Statement ( Alt+Num * ) — переносит в файл и текущую линию отлаживаемого скрипта. Например если файлов много, решили посмотреть что в других вкладках, а потом забыли где у вас отладка 🙂
Step Over ( F10 ) — делает один шаг не заходя внутрь функции. Т.е. если на текущей линии есть какая-то функция, а не просто переменная со значением, то при клике данной кнопки, отладчик не будет заходить внутрь неё.
Step Into ( F11 ) — делает шаг. Но в отличие от предыдущей, если есть вложенный вызов (например функция), то заходит внутрь неё.
Step Out ( Shift+F11 ) — выполняет команды до завершения текущей функции. Удобна, если случайно вошли во вложенный вызов и нужно быстро из него выйти, не завершая при этом отладку.
Restart ( Ctrl+Shift+F5 ) — Перезапустить отладку
Continue ( F5 ) — Продолжает выполнения скрипта с текущего момента. Если больше нет других точек останова, то отладка заканчивается и скрипт продолжает работу. В ином случае работа прерывается на следующей точке останова.
Stop Debugging ( Shift+F5 ) — Завершить отладку
Итак, в текущем коде видно значение входного параметра:
- data = "23 24 11 18" — строка с данными через пробел
- numsStringArr = <"23", "24", "11", "18">— массив строк, который получился из входной переменной.

Если нажмем F10 2 раза, то окажемся на строке 11; во вкладках Watch и Variables и в самой странице с кодом увидим, что nums была инициализирована и в ней лежит массив целых чисел <23, 24, 11, 18>.
Если теперь нажмем F10 , то попадем внутрь цикла foreach и нажимая теперь F10 пока не окончится цикл, можно будет наблюдать на каждой итерации, как значение num и sum постоянно изменяются. num теперь можно будет наблюдать во вкладке Watch , потому что сейчас она доступна внутри цикла. Тем самым мы можем проследить шаг за шагом весь процесс изменения любых переменных и значений на любом этапе, который интересует.
Дальнейшие нажатия F10 переместит линию кода на строки 15, 16 и, наконец, 20.
Дополнительно
Если перейти во вкладку Breakpoints в нижней панели, то можно посмотреть все брейкпойнты, удалить все брейкпойнты, включить/выключить, а также еще более тонко настроить условие, при котором на данной отметке надо остановиться. В методе выше, например, нужно остановиться только когда sum превысит значение 20.

Это удобно, если останов нужен только при определённом значении, а не всегда (особенно в случае с циклами).
Больше информации об отладке можно посмотреть в Сведения об отладке кода C# с помощью Visual Studio.
Как найти ошибки в C# проекте, работая под Linux и macOS

Некоторое время назад мой коллега Сергей Васильев написал заметку об открытии бета-теста новой версии разрабатываемого нами статического анализатора PVS-Studio. Сейчас бета-тест завершён и новую версию можно загрузить, перейдя по ссылке. В этой же статье мы рассмотрим, как проводится анализ C# проектов под Linux/macOS с использованием консольного интерфейса и Rider. После этого проведём традиционный разбор некоторых интересных срабатываний анализатора.
RavenDB

Для проверки был выбран открытый проект RavenDB, репозиторий которого насчитывает почти 5 тысяч файлов исходного кода. Он представляет собой достаточно популярную NoSQL-базу данных. Подробности можно узнать на сайте. Нетрудно догадаться, что меня привлёк его размер, ведь в столь большом и, вне всяких сомнений, серьёзном проекте наверняка найдётся что-нибудь интересное.
Command Line Interface
Для начала рассмотрим, как производится анализ через консоль. Этот раздел, на мой взгляд, будет особенно интересен тем, кто желает интегрировать анализатор в CI-систему. У команды, запускающей анализ, есть ряд интересных опций, однако в общем случае всё достаточно тривиально. Чтобы провести анализ RavenDB, я перехожу в папку с проектом и ввожу в консоли следующее:
Флаг -t (сокращение от target) служит для указания файла решения или проекта для проверки. Представленная выше строка запускает анализ и в результате работы формирует файл, содержащий найденные ошибки. Всё просто, не правда ли?
Rider
Работа с анализатором в Rider представляет собой примерно то же самое, что и в Visual Studio. Простой и понятный интерфейс плагина позволит проверить проект буквально в пару кликов. Это не преувеличение – для проведения анализа RavenDB мне было достаточно кликнуть в верхнем меню Tools, навести на «PVS-Studio» и кликнуть «Check Current Solution/Project».

Результаты анализа будут отображены в нижней части окна на вкладке PVS-Studio (ну а на какой же ещё? 🙂 )

Как и в случае с плагином для Visual Studio, двойной клик по предупреждению отобразит место, с которым оно связано. Всё удобно и понятно.
Что ещё более важно, инструмент PVS-Studio не просто указывает на ошибки, а имеет инфраструктуру, позволяющую легко внедрить методологию статического анализа даже в большой старый проект.
Общая идея в следующем. Пользователь запустил анализатор и получил множество предупреждений. Раз проект, разрабатываемый много лет, жив, развивается и приносит деньги, то, скорее всего, в отчёте не будет много предупреждений, указывающих на критические дефекты. Другими словами, критические баги так или иначе уже поправлены более дорогими способами или благодаря фидбеку от клиентов. Соответственно, всё, что сейчас находит анализатор, можно считать техническим долгом, который непрактично стараться устранить сразу. Рационально пока игнорировать эти предупреждения, но писать новый код уже выполняя регулярный анализ.
Можно указать PVS-Studio считать эти предупреждения пока неактуальными (отложить технический долг на потом), и больше их не показывать. Анализатор создаёт специальный файл, где сохраняет информацию о пока неинтересных ошибках. И теперь PVS-Studio будет выдавать предупреждения только на новый или измененный код. Причем, всё это реализовано умно. Если, например, в начало некоего файла будет добавлена пустая строка, то анализатор понимает, что, по сути, ничего не изменилось, и по-прежнему будет молчать. Этот файл разметки можно заложить в систему контроля версий. Файл большой, но это не страшно, так как часто его закладывать смысла нет.
Теперь все программисты будут видеть предупреждения, относящиеся только к новому или изменённому коду. Таким образом, анализатор можно начать использовать, что называется, со следующего дня. А к техническому долгу можно будет вернуться позднее, и постепенно исправлять ошибки и настраивать анализатор.
Для подавления предупреждений на существующий код в Rider необходимо просто перейти в верхнем меню в Tools->PVS-Studio и кликнуть «Suppress All Messages».

В появившемся окне, предупреждающем о том, что в suppress-лист попадут все текущие срабатывания, кликаем на «Ok». В результате в папке с проектом будет создан suppress-файл, который будет учитываться анализатором при дальнейшей работе.
Надо отметить, что в Rider уже есть анализатор, который успешно подсвечивает некоторые ошибки. Таким образом, ряд предупреждений PVS-Studio указывает на код, который выглядит подозрительно и с точки зрения редактора. Тем не менее, PVS-Studio довольно часто находит ошибки, которые смогли уйти от чуткого взора анализатора от JetBrains. Именно поэтому наиболее эффективным решением будет позволить им работать в команде.

А на десерт
Теперь, как и обещал, рассмотрим, какие интересные предупреждения отобразил анализатор по результатам проверки. Проект содержит огромное количество файлов исходного кода, поэтому неудивительно было найти в нём множество подозрительных моментов. Ничего не поделаешь – все совершают ошибки, однако важно прикладывать максимум усилий к их своевременному обнаружению и исправлению. Статический анализ позволяет существенно упростить эту задачу.
В результате проверки было выведено около тысячи предупреждений:

Подробнее об уровнях предупреждений можно прочитать, перейдя по ссылке.
Конечно, не все срабатывания указывают на супер-страшные ошибки. Если бы это было так, вряд ли бы в проекте хоть что-то работало :). Но важно понимать – раз анализатор «ругается», значит код выглядит странно, и его стоит внимательно изучить.
Вообще в проекте нашлось немало любопытных срабатываний. Тем не менее, не хотелось бы, чтобы статья получилась совсем уж огромной, поэтому мы рассмотрим только некоторые из них.
Всего лишь дополнительная проверка?
Предупреждение анализатора: V3022 Expression ‘dirpath == null’ is always false. PosixHelper.cs(124) Voron
Данное срабатывание можно рассмотреть по-разному. С одной стороны, лишняя проверка – это, конечно, не очень хорошо, но сама по себе она ошибкой не является. С другой – стоит задуматься: действительно ли этот код работает так, как задумывал программист?
Возможно, разработчик и правда не знал, что ToString никогда не вернёт null. Если же это не так, то можно сделать предположение о том, чего он хотел добиться.
Быть может, break должен вызываться, когда не удаётся получить родителя для рассматриваемой директории. Если это так, то проверка на null обретает смысл. Однако рассматривать нужно не результат работы ToString, а значение, возвращаемое методом GetParent:
В противном случае, возвращение null методом GetParent приводит к исключению при вызове ToString.
Типичный null
Предупреждение анализатора: V3125 The ‘item’ object was used after it was verified against null. Check lines: 249, 247. ActiveTransactions.cs(249), ActiveTransactions.cs(247) Voron
Код выглядит странно из-за происходящего в том случае, когда item действительно null. Действительно, если InvalidLowLevelTransaction тоже окажется null, условие окажется истинным и попытка получения item.Id приведёт к выбрасыванию исключения. А если InvalidLowLevelTransaction не может оказаться null, то условие «item == InvalidLowLevelTransaction» попросту лишнее. Всё потому, что оно проверяется лишь в случае, когда item == null. Ну а если вдруг item не может быть null, то вообще всё условие теряет смысл и лишь добавляет ненужную вложенность.
Я думаю, что здесь, возможно, выбран неверный логический оператор. Если заменить в условии «||» на «&&», то код сразу начинает выглядеть логично. Ну и никаких проблем в таком случае возникнуть не может.
Срабатывания подобного плана являются типичными представителями весьма опасных в теории ошибок, обнаруживаемых анализатором. Справедливости ради надо отметить, что анализатор, встроенный в Rider, тоже помечает данный фрагмент как потенциально опасный.
Снова лишняя проверка?
Предупреждение анализатора: V3022 Expression ‘_continuationState.Count > 0’ is always true. ManualBlittableJsonDocumentBuilder.cs(152) Sparrow
Сначала во внешнем условии проверяется, что количество элементов в коллекции больше 1, а затем на следующей же строке тернарный оператор проверяет, что их число больше 0. Создаётся впечатление, что одна из проверок должна выглядеть иначе. Так или иначе, этот код весьма подозрительно выглядит и должен быть внимательно изучен и переработан при необходимости.
Возможный NRE
Предупреждение анализатора: V3095 The ‘node.Indexer’ object was used before it was verified against null. Check lines: 1180, 1182. ExpressionStringBuilder.cs(1180), ExpressionStringBuilder.cs(1182) Raven.Client
На самом деле, это ещё одно место, которое считает подозрительным и PVS-Studio, и Rider. Правда, формулировки несколько отличаются: анализатор от JetBrains просто подсвечивает node.Indexer.DeclaringType с комментарием «Possible NullReferenceException».
Оба проверяющих утверждают, что в этом фрагменте действительно может быть выброшено исключение. Должен заметить, что предупреждение от PVS-Studio не только говорит, что тут возможна ошибка, но и объясняет, почему у него сложилось такое мнение. Вроде мелочь, а приятно.
На самом деле, не факт, что тут и правда допущена ошибка. Вполне возможно, что если node.Object == null, то node.Indexer точно задан. При этом допустима ситуация, когда node.Object и node.Indexer оба не равны null. Только в таком случае можно считать данное срабатывание обоих анализаторов ложным. Поэтому стоит внимательно проанализировать все возможные варианты.
А если копнуть глубже?
Предупреждение анализатора: V3022 Expression ‘command != null’ is always true. AsyncDocumentSession.Load.cs(175) Raven.Client
Предупреждение выдаётся из-за того, что метод CreateRequest никогда не возвращает null. В самом деле, достаточно взглянуть на его код, чтобы убедиться в этом:
В целом, эта проверка не такая уж и проблема, хотя может быть так, что метод раньше и правда возвращал null в определённых обстоятельствах, а теперь в случае чего бросает исключение. Кто знает, возможно, что вместо той проверки на null сейчас должен быть try-catch.
У вас может возникнуть вполне разумный вопрос: а где тут бросается исключение-то? Если их нет, то тогда перед нами всё-таки просто лишняя проверка, и никакой ошибки тут быть не может.
Но увы, при выполнении метода исключение действительно может быть выброшено, причём дважды. Сначала в методе IncrementRequestCount:
Затем – в конструкторе GetDocumentsCommand:
Традиционный copy-paste
Предупреждение анализатора: V3127 Two similar code fragments were found. Perhaps, this is a typo. SmugglerResult.cs(256), SmugglerResult.cs(253) Raven.Client
Сильно сомневаюсь, что хоть кто-нибудь увидел бы тут странность, взглянув на код. Функция состоит из 14 подобных условий и все переменные заканчиваются на Updated. Даже когда тут приведена малая её часть, ошибку видно не сразу.
Человеческий мозг буквально отказывается искать в таком коде что-либо. В то же время, PVS-Studio с легкостью обнаружил, что присваивается, скорее всего, совершенно не то, должно:
Логично, что в нижней строке справа от оператора присваивания должно быть ConflictSolverConfigUpdated. Я уверен, что без статического анализа данная странность была бы найдена только в случае, если бы из-за неё сломалось что-то достаточно серьёзное. Программист сможет заметить, что в этой функции скрыта проблема, разве что только если будет знать об этом заранее.
Ох уж этот «??»
Предупреждение анализатора: V3123 Perhaps the ‘??’ operator works in a different way than it was expected. Its priority is lower than priority of other operators in its left part. InMemoryDocumentSessionOperations.cs(1952) Raven.Client
Конечно, сохраняется возможность, что это вовсе не ошибка, что так всё и задумано. И всё же это место выглядит очень подозрительно. Ведь логично предположить, что идея функции состоит вовсе не в том, чтобы возвращать 0 при _onBeforeStoreDocumentsByEntity == null.
Я думаю, что здесь действительно присутствует ошибка, связанная с приоритетами операторов. В этом случае, необходимо поставить скобки:
Ну а если вдруг и правда всё так и задумано, то, возможно, стоит указать на это явно – тогда у анализатора и программистов, читающих код, вопросов не возникнет:
Но это уже дело вкуса, конечно.
Передача параметров
Предупреждение анализатора: V3066 Possible incorrect order of arguments passed to ‘ValidateLicense’ method: ‘newLicense’ and ‘oldLicense’. LicenseHelper.cs(177) Raven.Server
Аргументы переданы в метод в подозрительном порядке. В самом деле, взглянем на объявление:
Весьма приятно, что PVS-Studio способен находить даже такие ошибки. Это отличный пример к вопросу о преимуществах статического анализа перед динамическим.
Несмотря на вышесказанное, я поначалу предположил, что, возможно, не имеет значения, в каком порядке эти аргументы передавать. Конечно, в этом случае были бы не совсем корректно подобраны имена, но что ж поделать. Однако внутренняя структура ValidateLicense говорит о том, что эти параметры всё-таки имеют различный смысл. Код этой функции можно посмотреть, перейдя по ссылке.
Never continue
Предупреждение анализатора: V3022 Expression ‘GetCounterValueAndCheckIfShouldSkip(item.DocumentId, null, prop, out long value, out bool delete)’ is always false. RavenEtlDocumentTransformer.cs(362) Raven.Server
Полностью данный метод можно рассмотреть, перейдя по ссылке.
Предупреждение говорит о том, что вызов continue в этом цикле недостижим. И если это так, то фрагмент действительно странный. Но может это просто ложное срабатывание? Всё же Rider по этому поводу не ругается.
Взглянем на метод GetCounterValueAndCheckIfShouldSkip:
Очевидно, этот метод может вернуть true только в том случае, если function != null. В коде же, рассмотренном ранее, на место этого параметра передаётся именно нулевой указатель. Значит, вызов continue действительно недостижим.
Этот момент может быть как безобидным упущением, так и проблемой, связанной с какой-нибудь ошибкой в условии. Так или иначе, стоит обратить на него внимание.
Доверяй, но проверяй
Предупреждение анализатора: V3063 A part of conditional expression is always true if it is evaluated: Attributes != null. LicenseStatus.cs(28) Raven.Server
Крайне странный фрагмент. Обычно, лишние проверки хоть как-то отделены — здесь же соответствие переменной null-указателю проверяется прям в соседних строках. Создаётся впечатление, что код, вполне вероятно, делает совсем не то, что хотел программист.
Nullable, который никогда не null
Предупреждение анализатора: V3022 Expression ‘suspend.HasValue’ is always true. RachisAdminHandler.cs(116) Raven.Server
Очередная с виду безобидная «лишняя» проверка. Хотя пока не ясно, с чего анализатор считает её таковой.
Обратимся к GetBoolValueQueryString:
Действительно, иногда эта функция возвращает null. Да и Rider не считал ту проверку лишней. Неужели Единорог подвёл?

А что, если взглянуть на метод GetStringQueryString?
Хм, если параметр required == true, то будет вызван метод ThrowRequiredMember. Интересно, что же он делает? 🙂 Ну, чтобы уж точно не осталось сомнений:
Итак, резюмируем. Разработчик вызывает метод GetBoolValueQueryString. Вероятно, он считает, что потенциально метод не получит необходимое значение. Ну и как итог – вернёт null. Внутри вызывается GetStringQueryString. В случае если возникают проблемы, он либо вернёт null, либо выбросит исключение. Второе происходит в том случае, если параметр required установлен как true. При этом это его значение по умолчанию. В то же время, при вызове GetBoolValueQueryString он, если посмотреть на код выше, как раз не передаётся.
Давайте рассмотрим ещё раз код метода SuspendObserver, на фрагмент которого и ругался анализатор:
Создаётся впечатление, что по задумке поток выполнения тут не должен прерываться, если GetBoolValueQueryString не смог получить значение. В самом деле, после блока с проверкой на null производятся различные действия и возвращается значение. Полагаю, по задумке эти действия производятся независимо отуспешности работы метода GetBoolValueQueryString. Что же произойдёт на самом деле? Поток выполнения будет прерван исключением.
Чтобы этот момент поправить, нужно при вызове GetBoolValueQueryString передать в качестве второго параметра required значение false. Тогда всё действительно будет работать так, как ожидается.
Как я и говорил ранее, иногда кажется, что анализатор ошибается (чего греха таить, бывает и такое). Также достаточно часто предупреждение выглядит незначительным. Казалось бы, есть тут лишняя проверка, да и ладно. Ну или ещё лучше – уберём её и никаких проблем – предупреждение ведь пропадёт!
Даже в случаях, когда срабатывание кажется странным и непонятным, не стоит поспешно отмечать его как ложное. Нужно постараться понять, почему анализатор считает место проблемным и уже после этого принимать решение.
Странности
Предупреждение анализатора: V3061 Parameter ‘numberOfResults’ is always rewritten in method body before being used. DocumentHandler.cs(273), DocumentHandler.cs(267) Raven.Server
Параметр, передаваемый в функцию, не используется, а сразу перезаписывается. Зачем он тут нужен? Его хотели передавать через ref?
Мне стало любопытно взглянуть на то, как используется этот метод в существующем коде. Я понадеялся, что раз он приватный, их не должно быть особо много. Спасибо Rider, я с лёгкостью нашёл, где же производится вызов. Это было единственное место:
Переменной присваивается 0, затем она передаётся в метод, результат работы которого присваивается ей же. И внутри метода этот параметр никак не используется. Эм. Зачем всё это?

Не тот логический оператор
Предупреждение анализатора: V3022 Expression ‘me.Arguments.Count < 2 && me.Arguments.Count > 3’ is always false. Probably the ‘||’ operator should be used here. QueryMetadata.cs(861) Raven.Server
Полностью данный метод можно посмотреть здесь.
В этот раз перед нами очевидная ошибка, состоящая в использовании неправильного логического оператора. В текущем виде проверка количества аргументов попросту не работает, ведь нет такого значения, которое одновременно и меньше 2, и больше 3. Истинные же намерения разработчика легко раскрываются первым аргументом, передаваемым в конструктор исключения:
Странный try-метод
Предупреждение анализатора: V3063 A part of conditional expression is always true if it is evaluated: isMethod. QueryParser.cs(1797) Raven.Server
Полностью данный метод можно посмотреть здесь.
Конструкция var isMethod = Method(field, out var method) напомнила мне о стандартных методах вроде Int.TryParse. Такие методы производят попытку получить результат и записать её в out-переменную, а флаг успешности проведения операции является возвращаемым значением. Код, использующий такие функции, обычно производит проверку возвращаемого значения, а затем на её основе выполняет те или иные операции.
На мой взгляд, функция Method используется здесь именно таким образом. Результат работы Method, кроме того, является возвращаемым значением метода Operator, вызывающего её.
Анализатор же указывает, что переменная isMethod всегда будет иметь значение true и её проверка в условии бессмысленна. Это означает, что функция Method никогда не возвращает false. Какой же тогда смысл в использовании такой конструкции?
Для начала, убедимся в том, что анализатор не ошибся:
Действительно, возвращаемое значение этого метода всегда true. И если всё так и задумывалось, то это… странно, но не беда, в принципе. Но что, если нет?
Функция ReadMethodArguments, код которой можно посмотреть здесь, выбрасывает исключения в некоторых случаях. Такое происходит, когда метод не может корректно выполнить свою задачу.
Создаётся впечатление, что код, вызывающий функцию Method, не рассчитан на выбрасывание исключений. Скорее всего предполагается, что когда корректно выполнить получение значения out-переменной не удаётся, функция Method вернёт false. Однако при текущей реализации вместо этого будет выброшено исключение.
Как бы то ни было, стоит обратить внимание на данный фрагмент.
null != null?
Предупреждение анализатора: V3063 A part of conditional expression is always false if it is evaluated: m_curEdgeBlock != null. DotNetHeapDumpGraphReader.cs(803) Raven.Debug
Переменной присваивается нулевой указатель, а затем через несколько строчек производится проверка на её неравенство null. При этом бессмысленным становится код, производящий проверку nextEdgeBlock.Index != m_curEdgeBlock.Index + 1. Кроме того, никогда не произойдёт выбрасывание исключения.
Логично предположить, что тут что-то работает не так, как должно, ведь фрагмент выглядит очень странно. Либо проверка тут вообще не нужна, либо она реализована как-то неправильно.
Можно рассмотреть срабатывание и с другой стороны – пойти от обратного. Попробуем представить ситуацию, при которой это срабатывание является ложным. Я думаю, что это возможно, только если значение переменной может быть изменено при вызове Deque. Однако m_curEdgeBlock является приватным полем, а m_edgeBlocks – это стандартная очередь, которая инициализируется в этом же классе. Таким образом, весьма сомнительно, что вызов Dequeue как-то может повлиять на значение m_curEdgeBlock. Следовательно, срабатывание, скорее всего, не является ложным.
First or null
Предупреждение анализатора: V3146 Possible null dereference of ‘mainSchema’. The ‘Tables.FirstOrDefault’ can return default null value. DatabaseSchema.cs(31) Raven.Server
Срабатывание, на первый взгляд, может показаться непонятным. Действительно, при чём тут вообще FirstOrDefault? Чтобы стало ясно, отчего же анализатор ругается, необходимо взглянуть на функцию GetTable:
Вызов метода FirstOrDefault вместо First может быть обусловлен тем, что в коллекции может не быть элементов, соответствующих заданному условию. В таком случае FirstOrDefault, а, следовательно, и GetTable вернёт null, так как TableSchema – это ссылочный тип. Именно поэтому PVS-Studio и говорит о том, что в данном коде может произойти попытка разыменования нулевого указателя.
Возможно, всё же стоит проверять такой случай, чтобы выполнение не прерывалось с NullReferenceException. Если же вариант, при котором Tables.FirstOrDefault вернёт null невозможен, то тогда нет смысла в использовании FirstOrDefault вместо First.
Always true
Предупреждение анализатора: V3022 Expression is always true. Probably the ‘&&’ operator should be used here. DeleteCertificateFromClusterCommand.cs(21) Raven.Server
Ещё один пример ситуации, когда скорее всего выбран неправильный логический оператор. В данном случае условие всегда истинно, ведь переменная точно не равна хотя бы одному из значений, с которыми сравнивается.
Я полагаю, что «||» должен быть заменён на «&&». Тогда в написанном будет смысл. Если логический оператор всё же выбран верно, то скорее всего в одном из условий должно происходить сравнение других переменных. Так или иначе, этот фрагмент очень подозрительно выглядит и его обязательно нужно проанализировать.
Заключение
В первую очередь хочу поблагодарить всех, кто добрался до этого места. Статья вышла достаточно объёмная, но, надеюсь, вам было интересно вместе со мной разбираться с новой версией анализатора PVS-Studio и изучать найденные ошибки.
Важно помнить, что разработчик должен основной своей целью ставить вовсе не уменьшение количества срабатываний. Не ради достижения пустого лога ошибок нужно использовать PVS-Studio. Бороться со срабатываниями – всё равно что бороться с симптомами болезни, от которой страдает исходный код.
Рассматривая сообщения анализатора, всегда нужно стараться понять, почему выдаётся то или иное предупреждение. Лишь осознав логику, по которой анализатор выдал предупреждение, можно сделать вывод о том, указывает оно на ошибку или нет. Именно в этом случае вы будете бороться не с симптомом, а с болезнью. И именно таким образом ваш код станет чище и здоровее. И конечно же, проблем с таким прекрасным исходником будет намного меньше. Хотя лучше я пожелаю вам, чтобы их не было вообще 🙂


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikita Lipilin. How to find errors in a C# project working under Linux and macOS.
Как искать и исправлять ошибки в коде
Искать ошибки в программах — непростая задача. Здесь нет никаких готовых методик или рецептов успеха. Можно даже сказать, что это — искусство. Тем не менее есть общие советы, которые помогут вам при поиске. В статье описаны основные шаги, которые стоит предпринять, если ваша программа работает некорректно.
Шаг 1: Занесите ошибку в трекер
После выполнения всех описанных ниже шагов может так случиться, что вы будете рвать на себе волосы от безысходности, все еще сидя на работе, когда поймете, что:
- Вы забыли какую-то важную деталь об ошибке, например, в чем она заключалась.
- Вы могли делегировать ее кому-то более опытному.
Трекер поможет вам не потерять нить размышлений и о текущей проблеме, и о той, которую вы временно отложили. А если вы работаете в команде, это поможет делегировать исправление коллеге и держать все обсуждение в одном месте.
Вы должны записать в трекер следующую информацию:
- Что делал пользователь.
- Что он ожидал увидеть.
- Что случилось на самом деле.
Это должно подсказать, как воспроизвести ошибку. Если вы не сможете воспроизвести ее в любое время, ваши шансы исправить ошибку стремятся к нулю.
Шаг 2: Поищите сообщение об ошибке в сети
Если у вас есть сообщение об ошибке, то вам повезло. Или оно будет достаточно информативным, чтобы вы поняли, где и в чем заключается ошибка, или у вас будет готовый запрос для поиска в сети. Не повезло? Тогда переходите к следующему шагу.
Шаг 3: Найдите строку, в которой проявляется ошибка
Если ошибка вызывает падение программы, попробуйте запустить её в IDE под отладчиком и посмотрите, на какой строчке кода она остановится. Совершенно необязательно, что ошибка будет именно в этой строке (см. следующий шаг), но, по крайней мере, это может дать вам информацию о природе бага.
Шаг 4: Найдите точную строку, в которой появилась ошибка
Как только вы найдете строку, в которой проявляется ошибка, вы можете пройти назад по коду, чтобы найти, где она содержится. Иногда это может быть одна и та же строка. Но чаще всего вы обнаружите, что строка, на которой упала программа, ни при чем, а причина ошибки — в неправильных данных, которые появились ранее.
Если вы отслеживаете выполнение программы в отладчике, то вы можете пройтись назад по стектрейсу, чтобы найти ошибку. Если вы находитесь внутри функции, вызванной внутри другой функции, вызванной внутри другой функции, то стектрейс покажет список функций до самой точки входа в программу (функции main() ). Если ошибка случилась где-то в подключаемой библиотеке, предположите, что ошибка все-таки в вашей программе — это случается гораздо чаще. Найдите по стектрейсу, откуда в вашем коде вызывается библиотечная функция, и продолжайте искать.
Шаг 5: Выясните природу ошибки
Ошибки могут проявлять себя по-разному, но большинство из них можно отнести к той или иной категории. Вот наиболее частые.
- Неправильные настройки или константы
Проверьте ваши конфигурационные файлы и константы. Я однажды потратил ужасные 16 часов, пытаясь понять, почему корзина на сайте с покупками виснет на стадии отправки заказа. Причина оказалась в неправильном значении в /etc/hosts , которое не позволяло приложению найти ip-адрес почтового сервера, что вызывало бесконечный цикл в попытке отправить счет заказчику. - Неожиданный null
Бьюсь об заклад, вы не раз получали ошибку с неинициализированной переменной. Убедитесь, что вы проверяете ссылки на null , особенно при обращении к свойствам по цепочке. Также проверьте случаи, когда возвращаемое из базы данных значение NULL представлено особым типом. - Некорректные входные данные
Вы проверяете вводимые данные? Вы точно не пытаетесь провести арифметические операции с введенными пользователем строками? - Присваивание вместо сравнения
Убедитесь, что вы не написали = вместо == , особенно в C-подобных языках. - Ошибка округления
Это случается, когда вы используете целое вместо Decimal , или float для денежных сумм, или слишком короткое целое (например, пытаетесь записать число большее, чем 2147483647, в 32-битное целое). Кроме того, может случиться так, что ошибка округления проявляется не сразу, а накапливается со временем (т. н. Эффект бабочки). - Переполнение буфера и выход за пределы массива
Проблема номер один в компьютерной безопасности. Вы выделяете память меньшего объема, чем записываемые туда данные. Или пытаетесь обратиться к элементу за пределами массива. - Программисты не умеют считать
Вы используете некорректную формулу. Проверьте, что вы не используете целочисленное деление вместо взятия остатка, или знаете, как перевести рациональную дробь в десятичную и т. д. - Конкатенация строки и числа
Вы ожидаете конкатенации двух строк, но одно из значений — число, и компилятор пытается произвести арифметические вычисления. Попробуйте явно приводить каждое значение к строке. - 33 символа в varchar(32)
Проверяйте данные, передаваемые в INSERT , на совпадение типов. Некоторые БД выбрасывают исключения (как и должны делать), некоторые просто обрезают строку (как MySQL). Недавно я столкнулся с такой ошибкой: программист забыл убрать кавычки из строки перед вставкой в базу данных, и длина строки превысила допустимую как раз на два символа. На поиск бага ушло много времени, потому что заметить две маленькие кавычки было сложно. - Некорректное состояние
Вы пытаетесь выполнить запрос при закрытом соединении или пытаетесь вставить запись в таблицу прежде, чем обновили таблицы, от которых она зависит. - Особенности вашей системы, которых нет у пользователя
Например: в тестовой БД между ID заказа и адресом отношение 1:1, и вы программировали, исходя из этого предположения. Но в работе выясняется, что заказы могут отправляться на один и тот же адрес, и, таким образом, у вас отношение 1:многим.
Вы начали цикл for с единицы вместо нуля или наоборот. Или, например, подумали, что метод .count() или .length() вернул индекс последнего элемента. Проверьте документацию к языку, чтобы убедиться, что нумерация массивов начинается с нуля или с единицы. Эта ошибка иногда проявляется в виде исключения Index out of range .
Ваш процесс или поток пытается использовать результат выполнения дочернего до того, как тот завершил свою работу. Ищите использование sleep() в коде. Возможно, на мощной машине дочерний поток выполняется за миллисекунду, а на менее производительной системе происходят задержки. Используйте правильные способы синхронизации многопоточного кода: мьютексы, семафоры, события и т. д.
Если ваша ошибка не похожа на описанные выше, или вы не можете найти строку, в которой она появилась, переходите к следующему шагу.
Шаг 6: Метод исключения
Если вы не можете найти строку с ошибкой, попробуйте или отключать (комментировать) блоки кода до тех пор, пока ошибка не пропадет, или, используя фреймворк для юнит-тестов, изолируйте отдельные методы и вызывайте их с теми же параметрами, что и в реальном коде.
Попробуйте отключать компоненты системы один за другим, пока не найдете минимальную конфигурацию, которая будет работать. Затем подключайте их обратно по одному, пока ошибка не вернется. Таким образом вы вернетесь на шаг 3.
Шаг 7: Логгируйте все подряд и анализируйте журнал
Пройдитесь по каждому модулю или компоненту и добавьте больше сообщений. Начинайте постепенно, по одному модулю. Анализируйте лог до тех пор, пока не проявится неисправность. Если этого не случилось, добавьте еще сообщений.
Ваша задача состоит в том, чтобы вернуться к шагу 3, обнаружив, где проявляется ошибка. Также это именно тот случай, когда стоит использовать сторонние библиотеки для более тщательного логгирования.
Шаг 8: Исключите влияние железа или платформы
Замените оперативную память, жесткие диски, поменяйте сервер или рабочую станцию. Установите обновления, удалите обновления. Если ошибка пропадет, то причиной было железо, ОС или среда. Вы можете по желанию попробовать этот шаг раньше, так как неполадки в железе часто маскируют ошибки в ПО.
Если ваша программа работает по сети, проверьте свитч, замените кабель или запустите программу в другой сети.
Ради интереса, переключите кабель питания в другую розетку или к другому ИБП. Безумно? Почему бы не попробовать?
Если у вас возникает одна и та же ошибка вне зависимости от среды, то она в вашем коде.
Шаг 9: Обратите внимание на совпадения
- Ошибка появляется всегда в одно и то же время? Проверьте задачи, выполняющиеся по расписанию.
- Ошибка всегда проявляется вместе с чем-то еще, насколько абсурдной ни была бы эта связь? Обращайте внимание на каждую деталь. На каждую. Например, проявляется ли ошибка, когда включен кондиционер? Возможно, из-за этого падает напряжение в сети, что вызывает странные эффекты в железе.
- Есть ли что-то общее у пользователей программы, даже не связанное с ПО? Например, географическое положение (так был найден легендарный баг с письмом за 500 миль).
- Ошибка проявляется, когда другой процесс забирает достаточно большое количество памяти или ресурсов процессора? (Я однажды нашел в этом причину раздражающей проблемы «no trusted connection» с SQL-сервером).
Шаг 10: Обратитесь в техподдержку
Наконец, пора попросить помощи у того, кто знает больше, чем вы. Для этого у вас должно быть хотя бы примерное понимание того, где находится ошибка — в железе, базе данных, компиляторе. Прежде чем писать письмо разработчикам, попробуйте задать вопрос на профильном форуме.
Ошибки есть в операционных системах, компиляторах, фреймворках и библиотеках, и ваша программа может быть действительно корректна. Но шансы привлечь внимание разработчика к этим ошибкам невелики, если вы не сможете предоставить подробный алгоритм их воспроизведения. Дружелюбный разработчик может помочь вам в этом, но чаще всего, если проблему сложно воспроизвести вас просто проигнорируют. К сожалению, это значит, что нужно приложить больше усилий при составлении багрепорта.
Полезные советы (когда ничего не помогает)
- Позовите кого-нибудь еще.
Попросите коллегу поискать ошибку вместе с вами. Возможно, он заметит что-то, что вы упустили. Это можно сделать на любом этапе. - Внимательно просмотрите код.
Я часто нахожу ошибку, просто спокойно просматривая код с начала и прокручивая его в голове. - Рассмотрите случаи, когда код работает, и сравните их с неработающими.
Недавно я обнаружил ошибку, заключавшуюся в том, что когда вводимые данные в XML-формате содержали строку xsi:type=’xs:string’ , все ломалось, но если этой строки не было, все работало корректно. Оказалось, что дополнительный атрибут ломал механизм десериализации. - Идите спать.
Не бойтесь идти домой до того, как исправите ошибку. Ваши способности обратно пропорциональны вашей усталости. Вы просто потратите время и измотаете себя. - Сделайте творческий перерыв.
Творческий перерыв — это когда вы отвлекаетесь от задачи и переключаете внимание на другие вещи. Вы, возможно, замечали, что лучшие идеи приходят в голову в душе или по пути домой. Смена контекста иногда помогает. Сходите пообедать, посмотрите фильм, полистайте интернет или займитесь другой проблемой. - Закройте глаза на некоторые симптомы и сообщения и попробуйте сначала.
Некоторые баги могут влиять друг на друга. Драйвер для dial-up соединения в Windows 95 мог сообщать, что канал занят, при том что вы могли отчетливо слышать звук соединяющегося модема. Если вам приходится держать в голове слишком много симптомов, попробуйте сконцентрироваться только на одном. Исправьте или найдите его причину и переходите к следующему. - Поиграйте в доктора Хауса (только без Викодина).
Соберите всех коллег, ходите по кабинету с тростью, пишите симптомы на доске и бросайте язвительные комментарии. Раз это работает в сериалах, почему бы не попробовать?
Что вам точно не поможет
- Паника
Не надо сразу палить из пушки по воробьям. Некоторые менеджеры начинают паниковать и сразу откатываться, перезагружать сервера и т. п. в надежде, что что-нибудь из этого исправит проблему. Это никогда не работает. Кроме того, это создает еще больше хаоса и увеличивает время, необходимое для поиска ошибки. Делайте только один шаг за раз. Изучите результат. Обдумайте его, а затем переходите к следующей гипотезе. - «Хелп, плиииз!»
Когда вы обращаетесь на форум за советом, вы как минимум должны уже выполнить шаг 3. Никто не захочет или не сможет вам помочь, если вы не предоставите подробное описание проблемы, включая информацию об ОС, железе и участок проблемного кода. Создавайте тему только тогда, когда можете все подробно описать, и придумайте информативное название для нее. - Переход на личности
Если вы думаете, что в ошибке виноват кто-то другой, постарайтесь по крайней мере говорить с ним вежливо. Оскорбления, крики и паника не помогут человеку решить проблему. Даже если у вас в команде не в почете демократия, крики и применение грубой силы не заставят исправления магическим образом появиться.
Ошибка, которую я недавно исправил
Это была загадочная проблема с дублирующимися именами генерируемых файлов. Дальнейшая проверка показала, что у файлов различное содержание. Это было странно, поскольку имена файлов включали дату и время создания в формате yyMMddhhmmss . Шаг 9, совпадения: первый файл был создан в полпятого утра, дубликат генерировался в полпятого вечера того же дня. Совпадение? Нет, поскольку hh в строке формата — это 12-часовой формат времени. Вот оно что! Поменял формат на yyMMddHHmmss , и ошибка исчезла.
Как найти ошибку в коде c

| № | Пользователь | Рейтинг |
|---|---|---|
| 1 | B enq | 3783 |
| 2 | j iangly | 3669 |
| 3 | t ourist | 3658 |
| 4 | m aroonrk | 3650 |
| 5 | U m_nik | 3541 |
| 6 | f antasy | 3526 |
| 7 | R adewoosh | 3515 |
| 8 | k o_osaga | 3500 |
| 9 | i naFSTream | 3477 |
| 10 | c nnfls_csy | 3427 |
| № | Пользователь | Вклад |
|---|---|---|
| 1 | awoo | 181 |
| 2 | U m_nik | 180 |
| 3 | -is-this-fft- | 172 |
| 4 | nor | 171 |
| 5 | adamant | 167 |
| 6 | m aroonrk | 166 |
| 7 | antontrygubO_o | 161 |
| 8 | SecondThread | 160 |
| 9 | YouKn0wWho | 156 |
| 10 | kostka | 153 |
GlebsHP → Nebius Welcome Round Editorial
IanDeHaan → Teams Going to the 2023 ICPC North America Championship
GlebsHP → Nebius Welcome Round (Div. 1 + Div. 2, rated, t-shirts!)
Pakgamer2022 → Need Help!
AAK → Indian ICPC 2022-23 Regionals — Discussion
Vladosiya → Codeforces Round 855 (Div. 3) Разбор
Diegogrc → Companies interested in hiring a Competitive Programming background eng?
adamant → Unexpected application of cosines
sahal → ICPC Dhaka Regional and Preliminary Problemset and Editorials
Kolyanchick → Задача "Крош и битовые операции" — Объяснение решения
appu_nitd → Invitation to Decathlon 2023
orz → Nebius Welcome Round (Div. 1 + Div. 2) screencast and editorial
vovuh → Разбор Codeforces Round #653 (Div. 3)
cpdojo → Introducing cpdojo
OneShhhhhh → Introducing CF-Daily: An Easy Way to Discover Daily Codeforces Challenges
zibada → Google Code Jam Archive
nor → [Tutorial] Floors, ceilings and inequalities for beginners (with some programming tips)
ExplodingKonjac → A ridiculous user script for codeforces: Emojiforces
SirRembocodina → Обратный остаток / деление по модулю – Быстрый гайд
fjuengermann → Codeforces Live Bot (Telegram Bot)
M aksim1744 → Counting primes in $\tilde<<\cal O>>(n^<2/3>\,\,)$
Omkar2003 → Regarding TLE on CF
yh11 → Need help/tutor for preparing for big coding competitions
adamant → The 1st Universal Cup. Stage 7: Zaporizhzhia
Pety → I solved "Textmare" after 64 submissions
Блог пользователя dmkozyrev_thinks_slowly
Быстрый поиск глупых ошибок на C++ кратко и с примерами
Автор dmkozyrev_thinks_slowly, история, 3 года назад ,
Чтобы найти ошибку в коде нужно просто.
Первым делом нужно проверить переполнения типов данных, ошибки вроде = вместо == , то, что вы записываете значение long long в переменную int , и многое другое. Предупредить возникновение некоторых глупых ошибок можно включением всех предупреждений компилятора. Экстренно проверить код можно по этой ссылке, там есть примеры. Необходимо заменить код из примера на свой код, дождаться компиляции и пофиксить все проблемные места. Предупреждения не являются ошибками, но могут ими все-таки быть из-за невнимательности.
Затем можно протестировать свое решение при запуске на codeforces. Для этого в меню соревнования нужно нажать Запуск, откроется меню запуска, затем вставить свой код и проверить его на нескольких маленьких тестах при помощи компилятора Clang++17 Diagnostics.
Для теста 10000000 10000000 вы увидите следующее runtime error: signed integer overflow: 10000000 * 10000000 cannot be represented in type ‘int’
Для теста: 11 0 1 2 3 4 5 6 7 8 9 0 вы увидите следующее ==3752==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x1126fd98 at pc 0x00cb1d97 bp 0x1126fb34 sp 0x1126fb30 WRITE of size 4 at 0x1126fd98 thread T0 #0 0xcb1d96 in std::bas. Ошибка исполнения, код возврата 1
Затем с помощью компилятора GNU G++ можно перевести стандартную библиотеку в режим дебагга следующими макросами:
И, опять же, позапускать на различных тестах. Смотрите примеры.
В данном примере, при запуске обнаружится ошибка, и стандартная библиотека нам явно сообщит, что произошло не так:
Здесь мы сохраняем итератор, затем удаляем его из сета, а затем попытаемся разыменовать удаленный итератор: возникает ошибка:
Здесь мы используем стандартный алгоритм, который сливает два отсортированных массива в один линейно. Этот алгоритм подразумевает, что оба массива отсортированы, но мы допустили ошибку и не отсортировали второй массив. Увидим следующее:
Сохраняем итератор на начало вектора, а затем добавляем в конец еще один элемент. Происходит ресайз вектора, выделение новой памяти и вся старая память копируется в новое место, и очищается, поэтому итератор начинает указывать на удаленную память, и стандартная библиотека нам об этом сообщает:
Если вы решаете задачу, попробуйте протестировать самостоятельно на маленьких и средних тестах, возможно, написать более медленное, но точно правильное решение и сравнить на случайных тестах или перебрать все маленькие тесты.
Используйте assert(условие); для проверки инвариантов. Если условие ложно, то программа завершится с вердиктом "Ошибка исполнения". Это поможет в программах на 50-100 строк проверять самого себя.
Затем идет метод пристального взгляда, когда вы пытаетесь определить, что пошло не так, внимательно читая то, что вы написали, и перепроверяя. Для этих моментов полезно писать простой и легко читаемый код.
Следующие макросы помогут эффективно выводить в консоль переменные с их названиями и значениями, контейнеры вроде std::vector<X> и std::set<X> , а также пары значений вида std::pair<X,Y> .