23 мая 2017 г.

Инструменты отладки: System.Diagnostics, Output window, DebuggerDisplay, Edit and continue, Immediate Window и т.д

В данной статье я опишу способы, как упростить себе процесс создания приложения. Перечислю инструменты, которыми активно пользуюсь и опишу ситуации в которых они мне пригодились. Приступим!

Пространство имен System.Diagnostics 


Если говорить о инструментарии, который уже имеется в составе .NET Framework, тогда речь пойдут о классах лежащих в пространстве имен System.Diagnostics.* . Из всего многообразия классов я выделю только те, которые помогут нам в разработке.

Класс взаимодействия с отладчиком (Debugger).


Самый часто используемый мной класс это Debugger, дающий возможность взаимодействовать с отладчиком студии и постараюсь описать ситуации использования отдельных методов этого класса.

Метод Debugger.Launch() 


Launch
работает так по правилам:
  • Если вы находитесь в режиме отладки, то при вызове этого метода ничего происходить не будет
  • Если запустить ваше приложение вне студии, то при вызове этого метода появится диалог с предложением использовать один из предложенных в списке отладчиков. При этом продолжить работу без использования отладчика невозможно.

Простенький пример консольного приложения:

using System.Diagnostics;
 
namespace DebugFeatures
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Debugger.Launch();
        }
    }
}
 
Запустив по F5 в студии вы ничего не заметите, но запусти в проводнике увидите диалог:

В котором можете выбрать либо уже запущенную студию (у меня это 1-я строка), либо использовать новый экземпляр студии по вашему выбору.
Внимание! Если "New instance Microsoft ....", тогда весь код проекта будет не доступен. Поэтому желательно выбирать студию с отрытым вашим проектом.
Теперь по поводу жизненных ситуаций, когда этот метод пригодится:
  • При отладке двух процессов, когда ваше приложение запускает второе приложение, которое быстро "что то делает" и закрывается. При этом вы не успеваете сделать Debug -> Attach to process.
  • Допустим вы являетесь разработчиком какого то расширения или add-on для какой то системы. При запуске система подгружаете ваш модуль в память и передает ему управление. Встает вопрос, как же теперь отлаживать ситуацию в момент запуска модуля? В этом случае добавляем в код метода Debugger.Launch() и в момент запуска получаем возможность отладить процесс запуска.
    P. S. Если вы пользуетесь Thread.Sleep(15000) + Attach to process, то мои вам соболезнования :)
Важно! Стоит помнить что даже в релизной сборке будет отображаться окно с отладчиками, поэтому не забывайте удалять Debugger.Launch(), когда надобность в нем отпадает.
 

Метод Debugger.Break()


Break работает по правилам:
  • Если отладчик подключен, то происходит остановка при вызове методе, сравнимая остановке на breakpoint.
  • Если же отладчик не подключен и вызывается метод Break(), тогда будет показано окно с выбором отладчика, но при этом закрыв его приложение продолжит работу.

Пример кода:

using System.Diagnostics;
 
namespace DebugFeatures
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            int a = 1;
            while (a < 10 * 1000 * 1000)
            {
                a ++;
                if (a == 9*999* 999)
                {
                    Debugger.Break();
                }
            }
        }
    }
} 
 
На практике использовал этот метод довольна редко, но было дело, что студия "глючила" и breakpoint слетали после перезапуска. Выходом был этот метод.

 Output window и класс Debug.


Мне кажется, что люди не дооценивают окно Output внутри Visual Studio убирая его с рабочего пространства, а ведь это просто идеальная консоль для вывода событий происходящих в приложении и просто спасительный круг, если вы отлаживаете сложное многопоточное приложение. Либо пытаясь распутать какие то ситуации, где требуется анализ параметров и локальных переменных, что бы понять причину неисправности. Сейчас я постараюсь продемонстрировать как использовать Output, но при этом не загромождать им место.
Наблюдение показывает, что у всех программистов, которых я видел одна из ситуаций ниже:
  • Окно Output не присутствует на видимом рабочем пространстве. А находится где то снизу на вкладках, при этом программист может иногда поглядывать туда.
  • Окно Output всегда видно, но имеет настолько малый размер, что в нем ничего не понятно. При сборке что то мелькает, а на вопрос "Зачем тебе оно если ничего не понятно?" получал ответ "Что бы знать, что студия не подвисла и было на что поглядеть". 
  • Последний вариант, программист активно пользуется им и читает, что пишется в окне. Но при этом в окне два scroll умещается 4-6 строчек, а при работе приложение внутри мелькает текст и бедняга программист пытается найти нужное место.
Если вы из пункта 3 тогда хотел бы предложить мой способ размещения окна, которым я пользуюсь и перенастраиваю везде уже так в течении 5 лет. На всё про всё уходит 5-10 минут, но бывают и такие которые годами будут пользоваться неудобным интерфейсом или способом, при этом зная, что правится проблема в считанные минуты, но человеку "Лениво разбираться" :)

Пошаговая инструкция:
  1. Открепляем окно Ouput и перетаскиваем его в правую часть окна, закрепляя ее на фиксаторе как на картинке:

  2. Отжимаем скрепку тем самым делая окно автоматически скрываемым. Скрепа находится в противоположной стороне от желтого заголовка окна Output.
  3. Теперь можно спокойно отрегулировать ширину Output окна. У меня в раскрытом варианте оно занимает пол экрана. Не стоит жадничать с ширины, когда в окне появится сообщения из лога то ширина вам покажется идеальной, так как не потребуется пользоваться горизонтальным scroll.
  4. Далее окно Output полезно когда идет сборка проектов, но оно может при сборке не показываться, если не указать это явно в настройках. Поэтому идем Tools -> Options и там переходим в раздел "Projects and Solution" и проставляем галку "Show Ouput window when build starts". И вуаля! Output при работе не занимает место, а при сборке и запуске он автоматически станет виден.
  5. Последний штрих, это убрать лишние сообщения в Debug. Что бы это сделать запускаем приложение, открываем Output нажимаем правую кнопку по окну и снимаем галки с пунктов показанных на картинке:

  6. Опционально, но советую потратить 1 минуту и установить VSColorOutput расширение, которое из Ч\Б вывод сделает колоритным, при этом сообщения содержащие слова *Exception*, *Error* и прочее будут выделяться и отображаться красным цветом. Тем самым привлекая внимание, это очень-очень полезная штука, когда в логе сыпется много сообщений. Так же можно самому задать в настройках слова, которые носят признак ошибок, но всегда хватало значений по умолчанию.
Теперь мы будем видеть вывод через Debug.Write и настроенный Debug.Trace.

Настройка Log4net и TraceAppender к нему:

    <appender name="TraceAppender" type="log4net.Appender.TraceAppender">
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date{HH:mm:ss} : %message%newline" />
      </layout>
    </appender>

Отлично не правда ли?
Теперь поговорим о Debug.Write\WriteLine. Этот класс я советую использовать при отладке так как он, в отличии от Trace.Write\WriteLine, пишет только в Output отладчика. Что бы стало понятно, если вы будете использовать какой то сторонний отладчик (WinDbg, IDA Pro и тому подобные), то вы эти сообщения увидите и в них output, даже не имея исходных кодов программы.

По поводу того, как Debug.Write жизнь и нервы спасает, ситуации из жизненного опыта:
  • Первая ситуация, это особенности работы с UI, когда тебе требуется при открытом PopUp или при наличии фокуса в каком то контроле понять, какие события вызываются в CodeBehind иногда хватает breakpoint, но иногда какой то кейс наступает в случае, если мы не убираем фокус с формы, а как тогда отлаживать код, если фокус убирать нельзя? В исключительных ситуациях приходится использовать Debug.Write что бы увидеть точную синхронную последовательность вызовов, тем самым понять, что за чем следует.
  • Допустим у нас создается большое количество объектов, и нам необходимо узнать значение параметра в каждом из них. Вариант поставить точку останова и на каждом вызове смотреть и копировать значения - это слишком долго. Поэтому достаточно выводить в Debug Output тем самым имея возможность скопировать весь текст от туда и произвести нужный анализ.
  • Еще один из плюсов, именно Debug.Write - если вдруг забыли и не потерли вывод сообщений, то ваш спам не засветится в логе у заказчика.

Атрибут DebuggerDisplayAttribute.


Представьте жизненную ситуацию, у вас имеется ссылка на массив из объектов и вам надо как то посмотреть значение его полей. Для этого вам потребуется нажимать на стрелочку напротив каждого элемента, что бы увидеть внутренние поля которые вам нужны. Если у вас 3-5 объектов то это можно проделать вручную, но представьте, что там 100 объектов, а вам надо найти тот, у которого определенное значение поля. В итоге либо писать отдельный код который бы отфильтровал вам объекты и оставит нужный. Или можно использовать замечательный атрибут DebuggerDisplayAttribute, который, в режиме отладки, покажет вам всю необходимую для вас информацию об объекте.

Постараюсь продемонстрировать работу на примере. Имеется код:

internal class Program
    {
        private static void Main(string[] args)
        {
            var names = new[] {"Sergei""Misha""Andrey"};
            var random = new Random();
            var people =
                Enumerable.Range(0, 25)
                    .Select(i => new Man(names[random.Next(names.Length)], random.Next(50)))
                    .ToArray();
            
            Debugger.Break();
        }
    }
 
    internal class Man
    {
        /// <summary>
        ///     Constructor.
        /// </summary>
        public Man(string name, int age)
        {
            Name = name;
            Age = age;
        }
 
        /// <summary>
        ///     Age.
        /// </summary>
        public int Age { getprivate set; }
 
        /// <summary>
        ///     Name.
        /// </summary>
        public string Name { getprivate set; }
    }
 
После запуска в массиве people у вас будут лежать объекты Man со случайными значениями поля возраст. Допустим ваша задача узнать, есть ли объекты с именем "Sergei" в массиве. Запускаем приложение встаем на точке останова, выделяем people жмем "Quick Watch" и видим, что для того, что бы узнать имя нам потребуется у каждого объекта отрывать поля:

По умолчанию отладчик выводит значение .ToString() объекта, он у нас не перегружен, поэтому ToString() выводит значение базового Object.ToString() выводящего имя класса. Поэтому я добавлю атрибут DebuggerDisplay к классу Man. Пример:

[DebuggerDisplay("Name = {Name}, Age = {Age}")]
internal class Man
{
    //....
}
 
Запустился и в "Quick Watch" видим совершенно другую картину:



Как это работает?

Очень просто, для понимания строку "Name = {Name}, Age = {Age}" стоит представлять как string.Format("Name = {0}, Age = {1}", Name, Age), где на место фигурных скобок { } будет подставлено значение поля с именем Name и Age. Так же можно использовать выражение и даже вызывать методы, как если бы мы работали с объектом напрямую. 

Что бы стало все еще более понятно постараюсь продемонстрировать строку посложнее:
[DebuggerDisplay("Name {Name}, NameUpperCase={GetUpperCaseText(Name)} Name length = {Name != null ? Name.Length.ToString() : \"Name empty\"}, Name startWith {Name != null ? Name[0].ToString() : \"Name empty\"}")]
    internal class Man
    {
        /// <summary>
        ///     Constructor.
        /// </summary>
        public Man(string name, int age)
        {
            Name = name;
            Age = age;
        }
 
        /// <summary>
        ///     Age.
        /// </summary>
        public int Age { getprivate set; }
 
        /// <summary>
        ///     Name.
        /// </summary>
        public string Name { getprivate set; }
 
        private string GetUpperCaseText(string str)
        {
            return (str ?? string.Empty).ToUpper();
        }
    }
 
Запустившись и увидим вот такую картинку:


Таким образом можно пластично настроит вывод любой интересующей нас информации.

Особенности, которые стоит всегда держать в голове:
  • Если свойство или поле вашего класса имеет значение NULL, а вы обратитесь к его свойству открыв окно отладчика, тогда будет NullReferenceException, который отобразится строкой. Поэтому, что бы DebuggerDisplay отображал все объекты корректно, стоит обрабатывать обращение к NULL полю.
  • Перед добавлением значения стоит помнить, что если обращение к свойству или методу приводит к запросу к БД или обращение к серверу, то и очевидно, что перед показом отладчик будет делать то же самое. Будьте к этому готовы.
  • Все изменения с полями\свойствами\объектами вызванными внутри форматированной строки останутся, как если бы вы сами это поменяли. Будьте внимательны!

Перемещение вектора исполнения в Visual Studio.


Попробую рассказать жизненные ситуации и способ, с помощью которого, почти всегда можно решить проблемы.
  1. Имеется метод, допустим импорта чего либо, выполняющийся длительное время. В конце метода есть место, которое требуется отладить. Мы ставим breakpoint, запускаем импорт и в хоте отладки по F10 понимаем, что мы прошли место, которое надо было изучить более пристально. Как решить эту проблему? Пока что, если только перезапустить импорт и вновь дойти до этого места.
  2. Допустим в импорте из пункта 1. есть условие IF+ELSE внутрь IF зайти легко, а что бы выполнилось условие ELSE требуется еще какие то сложные подготовительные действия. Можно за комментировать условие IF оставить только вызов ELSE, но хардкод это не очень удобный вариант.
  3. Вы находитесь в методе, и хотите произвести повторную отладку, но для этого вам понадобится по новой запустить приложение, а значит затратить лишнее время на ожидание и можете банально забыть, что же вы хотели проверить. 
Эти и другие ситуации можно решить очень простым способом. Произведя перемещение вектора исполнения (желтую стрелочку) на позицию доступную из текущего места, тем самым продолжить отладку с другого места..

Демонстрирую на примере кода:
internal class Program
    {
        private static void Main(string[] args)
        {
            string output;
 
            if (args.Any())
            {
                output = Method1();
            }
            else
            {
                output = Method2();
            }
 
            Console.WriteLine(output);
        }
 
        private static string Method2()
        {
            return "Method2";
        }
 
        private static string Method1()
        {
            return "Method1";
        }
    }
Как это делать на видео:



Это обычное консольное приложение, на нем видно как я вектор исполнения перемещаю в нужное мне место и код продолжает выполняться, как если бы было сделано GOTO. При этом передвигаясь вперед или назад, это не откат назад или пропуск операций и остановка в нужном месте Все действия это реальные переходы, и вполне можно так пропустить инициализацию какой то переменно, перейти к обращению не инициализированной переменной, то получите NullReferenceException в 100% рабочем коде. Поэтому перед тем как перемещать стоит внимательно посмотреть является ли переход "Корректным". Если хотите пройти метод снова, то обязательно переводите исполнение на повторную инициализацию локальных переменных.
Штука классная и активно используется мной.

Правка кода в режиме отладки [Edit and continue].


Хочу рассказать о еще одной feature в Visual Studio, которая может упростить процесс написания кода, когда тебе надо написать работу с классом, но тебе не известно, что хранится в его полях. В этом случае написание сводится к череде:
  • "Запуск" приложения и проход до места работы с объектом.
  • Изучение значения одного из полей
  • Остановка приложения, с целью внести правки или дописать код для работы с объектом.
  • Повтор первого шага
Либо еще одна ситуация: 
  • Идет отладка кода
  • Мы проваливаемся в какой то метод и замечаем, что в нем нужно не забыть сделать какую то несущественную правку.
  • При этом отладка основного сценария продолжается, а когда отладка завершится, не факт что сможешь вспомнить, что хотел ещё подправить
Если студия при попытке внести изменения в режиме отладки ругается и выводит сообщение "Managed Compatibility Mode does not support Edit and Continue":

Тогда хочу обрадовать, что эта проблема решаема, и дебажить и править можно одновременно, для этого идем в Tools > Options > Debugging > Uncheck "Enable Edit and Continue" (снимаем галку), после чего Visual Studio должна разрешить вам править код и находиться в режиме отладки.


Как я пользуюсь этим? 
  1. Ставлю точку останова в месте, где интересующий меня объект доступен по ссылке.
  2. Запускаю решение и дохожу до точки останова
  3. Затем уже в месте, где собираюсь писать код для работу с этим объектом начиная подсматриваю по мере необходимости его поля.
Данная схема не раз спасала мне нервы и время!
Внимание! Код который был написан не компилируется на лету, и при проходе через него возможны визуальные искажения, не стоит на это обращать внимание, так как Visual Studio оперирует данными по смещениям и размерам методов из pdb, которые были собраны до запуска.

Палочка выручалочка "Immediate Window".


За всё время пока я занимаюсь программированием под .NET Framework я ни разу не видел, что бы кто то при мне пользовался "Immediate Window" в Visual Studio, поэтому если ты слышишь про него первый раз, то обязательно добавь его на рабочее пространство и используй! А вот как, я расскажу прям сейчас.

Представьте ситуации, мы стоим на точке останова и вдруг:
  • Появляется необходимость вызвать какой то метод доступный из текущего места.
  • У локальной переменной хотим заменить значение БЕЗ перезапуска приложения
  • Посмотреть результат выполнения
  • Иными словами хочется вставить и исполнить какую то временную инструкцию.
Думаете это не возможно? Вы ошибаетесь - выход это "Immediate Window". Встав на точку останова вы можете писать не просто выражения, вызывать методы, но и менять значения переменных локальных и глобальных. При этом данный код будет выполняться отдельно от вашего исходного текста программы.

Пример использования:


Из уведенного стоит заметить такие вещи:
  • В самом окне работает IntelliSence поэтому проблем с именами быть не должно.
  • Само окно в случае, если введенное выражение возвращает какой то сложный объект, используется продвинутый Formatter, который покажет в читабельном виде все его поля.
  • Не обязательно указывать ";" после каждого выражения. 
  • Очень удобно использовать когда надо вывести все поля в читабельном виде.
  • Стрелками "Вверх\вниз" можно подставить ранее введенные выражения.
  • Можно задавать значения локальных переменных
Данное окно очень спасало, когда требовалось в режиме реального времени проверить передачу какого то хардкодного параметра методу, либо заменить значение локальной переменной на что то нужное.

Окно крутое, пользоваться обязательно!

Остановка по требованию или Condition breakpoints.


Breakpoint
пользуются все, но стоит помнить о том, что можно указать условие срабатывания точки останова. Бывает часто, что метод вызывается N раз, и требуется на K итерации проверить какой то значение. Если по каким то причинам вы не знает про Condition breakpoints то погнали!
Допустим есть код:

internal class Program
    {
        private static void Main(string[] args)
        {
            for (int i = 1; i < 1000; i++)
            {
                Process(i);
            }
        }
 
        private static void Process(int i)
        {
            i += new Random().Next(1000);
 
            Console.WriteLine(i);
        }
    }

Наша цель, когда i будет равно 500 остановиться и узнать полученное значение от суммы со случайным числом. Если вы садомазахист, то можете поставить точку останова и 500 раз нажимать F5. Но лучшим решением будет поставить точку останова в начале метода, выбрать "Conditions...":


В появившемся окне в поле "условие" вводим "i == 500" и если условие указано без ошибок, то по нажатию на Enter красная точка поменяет свой вид на точку с плюсом.


Запускаем программу и видим, что мы остановились на месте, когда значение i стало 500.



Внимание! Наблюдается падение производительности приложения, когда присутствуют данные точки останова, поэтому после окончания работы следует обязательно удалять их всех, потому что даже в выключенном режиме они создают тормоза.

Удалённый отладчик Remote Debugger.


Допустим вам в какой то ситуации потребовалось отладить вашу программу на другом компьютере. Вы скачиваете на нее исходный код, устанавливаете студию и начинаете отладку. В таком случае есть возможность работать в студии, которая стоит на вашем компьютере, а к удалённому компьютеру, для отладки, подключаться через Remote Debugger Tools.

Важно знать! Что нужно ставить Remote Debugger Tools для версии вашей студии, обратную совместимость версии не гарантируется. Поэтому если у вас к примеру Visual Studio 2012, тогда нужно искать Remote Debugger для 2012.

Всю настройку описывать не буду, по адресу Remote Debugging можно получить всю интересующую информацию.

Отладка без полного исходного кода приложения

Ситуация, когда требуется у заказчика отладить какой то код или модуль, при этом у вас есть только RDP доступ к рабочей машине и все остальные порты закрыты, и никакой Remote Debugger не поможет. В этой ситуации хочешь не хочешь придётся поставить Visual Studio, благо у неё есть триальный период, но встаёт вопрос, что делать с исходными кодами? Переносить весь код вашего решения, за которое заказчик платит деньги как то стрёмно, но есть выход. В случае если требуемое место находися в отдельном модуле тогда можно ограничиться переносом его и далее делая Debug > Attach to process и расставив точки останова можно отладить его.Но допустим у вам известен файл, который надо отладить в таком случае не надо копировать весь исходный код модуля. Достаточно скопировать только файл MyModule.cs на стенд заказчика, отрываем его студией цепляемся к работающиму процессу ставим точки останова и отлаживаемся!

При этом требуется две вещи:
  1. Сборка в работающем приложении должна быть /debug
  2. *.pdb файл должен находиться рядом с отлаживаемым модулей и должен быть актуальным.
Теперь расскажу подробнее на примере:
  • Допустим у нас есть файл BusinessModule.cs, который содержит код и требует отладки

  • Для понимания, что произойдёт привожу код Program.cs:

    namespace DebugFeatures
    {
        internal class Program
        {
            private static void Main(string[] args)
            {
                var module = new BusinessModule();
     
                Console.ReadKey();
     
                module.DoSomething();
            }
        }
    }
    
  • Теперь я смогу запустить приложение и по нажатию любой кнопки произойдёт вызов DoSomething() нашего файла. Теперь я для правдоподобности переношу файл BusinessModule.cs в другой раздел жёсткого диска.

  • Затем отрываем файл в Visual Studio. Если вы хотите локально проверить, то закройте студию в которой этот файл уже отрыт, иначе эта студию будет открываться первой. В результате мы видим отрытый файл без .sln и других файлов проекта.

  • Всё готово для теста, я в папке Bin запускаю ранее скомпилированный executable file. В студии захожу Debug > Attach to process выбираю запущенный процесс жму Attach ставлю точку останова, в приложение нажимаю любую клавишу и видим, что мы благополучно встали на точку останова, при этом у нас отрыт один лишь файл:

Таким образом, мы относительно безопасно можем отладить участок программы без "засветки" всего исходного кода.

2 комментария:

  1. Спасибо. Познавательно! Подскажите как в параграфе "Палочка выручалочка "Immediate Window"" вы включили отображение переменных output1, output2

    ОтветитьУдалить
    Ответы
    1. Очень рад если подчерпнули что то новое для себя. Переменные очень просто, подведя мышку к переменной появляется окошко со значением переменной, так вот напротив каждого значения если подводить мышку появляется скрепка, достаточно выбрать нужные вам поля и они останутся на экране

      https://www.oreilly.com/library/view/mastering-visual-studio/9781787281905/assets/67dbf13a-9177-4371-aca3-5b249d4070ef.png

      Удалить