c#, visual-studio, интеграция отладки в код

194
03 июня 2021, 10:00
  1. Существуют ли наработанные подходы к интеграции отладки в код?

  2. Если существуют, то какие?

  3. Если не существует, то есть ли какие-нибудь идеи?

Узнал, что в visual-studio можно жестко прописывать в коде точки останова. Пришла идея объединить вызов точки останова из кода с выводом в output. Причем, с выводом названия метода и номера строки. То, что получилось напишу в ответе. Сразу помогло найти какие-то ошибки в своем коде.

  1. Целесообразно ли вообще использовать подобный подход?

  2. Целесообразно ли повсеместно закладывать подобный подход на этапе до отладки?

  3. Чего не достает в описании концепции или как можно ее усовершенствовать или что лишнее?

Answer 1

Отчасти основано на вызове Debugger.Break(). Если кратко, то использовать можно так:

           if (this.Debug())
               Debugger.Break();

Механизм:

Интерфейсом IWithBreakpoints могут быть наделены классы, чтобы можно было использовать механизм:

    public interface IWithBreakpoints
    {
        bool EnableBreakpoints { get; set; } // локальный флаг вкл/откл
        int NestingLevel { get; }            // уровень вложенности 
        string GetDebugInfo();               // строка с инфо об объекте
    }

Сам Breakpoints, отчасти основан на extension-методе-расширении bool Debug(), в котором одновременно делается (если надо) вывод инфо в output и возвращается флаг необходимости вызова точки останова:

    public static class Breakpoints
    {
        public static bool Enabled { get; set; }
        public static bool WritingEnabled { get; set; }
        static Breakpoints()
        {
            Enabled = false;
            WritingEnabled = true;
        }
        public static bool Debug(this IWithBreakpoints @object)
        {
            bool shouldWrite = false;
            bool shouldBreak = false;
#if DEBUG
            shouldWrite = WritingEnabled;
                && @object.EnableBreakpoints
                ;
            shouldBreak = Enabled
                && @object.EnableBreakpoints
                && Debugger.IsAttached
                ;
#endif
            WriteLineIf(shouldWrite, @object.GetDebugInfo(), @object.NestingLevel);
            return shouldBreak;
        }
        // ---------------------------------------------
        // .... реализации WriteLine(..) основанных на Debug.WriteLine(..) 
        // .... с добавлением названия метода и номера строки  
    }

Если более подробно, то использовать так:

    public class TreeItem : IWithBreakpoints
    {
        public string Name { get; } = Guid.NewGuid().ToString().Substring(1,4);
        public TreeItem Parent { get; set; }
        public List<TreeItem> Children { get; } = new List<TreeItem>();
        public TreeItem(TreeItem parent, int childrenCount)
        {
            NestingLevel = parent != null ? parent.NestingLevel + 1 : 0;
            if (this.Debug())
                Debugger.Break();
            Parent = parent ?? null;
            if (childrenCount == 0)
                return;
            for (int i = 0; i <= childrenCount-1; i++ )
            {
                Children.Add(new TreeItem(this, childrenCount - 1));
            }
            if (this.Debug())
                Debugger.Break();
        }
        public void Traverse(Action<TreeItem> action)
        {
            if (this.Debug())
                Debugger.Break();
            action(this);
            foreach(var child in this.Children)
            {
                child.Traverse(action);
            }
        }
        //  --------------------------
        public bool EnableBreakpoints { get; set; } = true;
        public int NestingLevel { get; set; }
        public string GetDebugInfo()
        {
            return $"{Name} (ChildrenCount={Children.Count}): ";
        }
    }

И так:

    class Program
    {
        static void Main(string[] args)
        {
            Breakpoints.Enabled = true;
            Breakpoints.WritingEnabled = true;
            Debug.WriteLine("------------------------------------");
            var item = new TreeItem(null, 3);
            Debug.WriteLine("------------------------------------");
            item.Traverse(it => Console.WriteLine(it.Name));
            Console.ReadLine();
        }
    }

Тогда, остановившись на каждой Debugger.Break(), программа выведет в output следующие строки:

------------------------------------
####  [02:58:59.823]: 0025 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:03.498]: . 95d4 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:04.360]: . . 1136 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:05.321]: . . . d8aa (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:06.152]: . . 1136 (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 31
####  [02:59:07.056]: . . 2585 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:07.698]: . . . 6b50 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:08.273]: . . 2585 (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 31
####  [02:59:08.921]: . 95d4 (ChildrenCount=2):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 31
####  [02:59:09.592]: . f608 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:10.140]: . . 1d3e (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:10.720]: . . . 89ca (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:11.300]: . . 1d3e (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 31
####  [02:59:11.891]: . . 1e46 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:12.554]: . . . b09a (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:13.511]: . . 1e46 (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 31
####  [02:59:14.314]: . f608 (ChildrenCount=2):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 31
####  [02:59:15.105]: . ce17 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:15.657]: . . 67c2 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:16.129]: . . . a049 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:16.681]: . . 67c2 (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 31
####  [02:59:17.207]: . . 5827 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:17.681]: . . . 2c55 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 20
####  [02:59:18.144]: . . 5827 (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 31
####  [02:59:18.777]: . ce17 (ChildrenCount=2):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 31
####  [02:59:19.425]: 0025 (ChildrenCount=3):   #### :       в BreakpointsIdea.TreeItem..ctor(TreeItem parent, Int32 childrenCount) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 31
------------------------------------
####  [02:59:23.251]: 0025 (ChildrenCount=3):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:26.377]: . 95d4 (ChildrenCount=2):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:27.407]: . . 1136 (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:28.081]: . . . d8aa (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:28.865]: . . 2585 (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:29.591]: . . . 6b50 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:30.298]: . f608 (ChildrenCount=2):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:31.002]: . . 1d3e (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:31.616]: . . . 89ca (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:32.184]: . . 1e46 (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:32.867]: . . . b09a (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:33.465]: . ce17 (ChildrenCount=2):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:33.961]: . . 67c2 (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:34.327]: . . . a049 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:34.617]: . . 5827 (ChildrenCount=1):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37
####  [02:59:34.820]: . . . 2c55 (ChildrenCount=0):   #### :       в BreakpointsIdea.TreeItem.Traverse(Action`1 action) в C:\Users\User\source\repos\BreakpointsIdea\BreakpointsIdea\Program.cs:строка 37

По вопросам:

  1. Существуют наработанные подходы или нет: не знаю, сходу не нашел.

  2. Какие: не знаю.

  3. Есть ли какие-нибудь идеи: то, что в ответе.

  4. Целесообразно ли вообще использовать подобный подход: думаю, иногда можно.

  5. Целесообразно ли повсеместно закладывать: не знаю.

  6. Чего не достает: если таких точек много, то как их лучше отключать, или наоборот, как включать только нужные? Если строку GetStackTraceLine() использовать как ключ, то можно было бы организовать временный tmp файл, где хранить флаг включен/отключен для каждой такой точки останова. И менять его при остановке прямо в Quick-Watch? По истечении времени чтобы эти точки сами дисэйблились? Или это навороты?

GetStackTraceLine():

        private static string GetStackTraceLine()
        {
            using (StringReader sr = new StringReader(Environment.StackTrace))
            {
                string line;
                while ((line = sr.ReadLine()) != null)
                {
                    if (line.Contains("."+nameof(Breakpoints)+".")
                        || line.ToLower().Contains(nameof(Environment.StackTrace).ToLower())
                        || line.Contains(nameof(Environment)))
                        continue;
                    else
                        return line;
                }
                return string.Empty;
            }
        }

Весь код механизма:

    public interface IWithBreakpoints
    {
        bool EnableBreakpoints { get; set; }   // локальный флаг включения отключения механизма
        int NestingLevel { get; }              // уровень вложенности 
        string GetDebugInfo();                 // строка с информацией об объекте для вывода
    }
    public static class Breakpoints
    {
        public static bool Enabled { get; set; }
        public static bool WritingEnabled { get; set; }
        static Breakpoints()
        {
            Enabled = false;
            WritingEnabled = true;
        }
        public static bool Debug(this IWithBreakpoints @object)
        {
            bool shouldWrite = false;
            bool shouldBreak = false;
#if DEBUG
            shouldWrite = WritingEnabled;
                && @object.EnableBreakpoints
                ;
            shouldBreak = Enabled
                && @object.EnableBreakpoints
                && Debugger.IsAttached
                ;
#endif
            WriteLineIf(shouldWrite, @object.GetDebugInfo(), @object.NestingLevel);
            return shouldBreak;
        }
        // ---------------------------------------------
        // ....  WriteLine(..) implementations (appends method name and line number from Environment.StackTrace)
        private static string GetStackTraceLine()
        {
            using (StringReader sr = new StringReader(Environment.StackTrace))
            {
                string line;
                while ((line = sr.ReadLine()) != null)
                {
                    if (line.Contains("."+nameof(Breakpoints)+".")
                        || line.ToLower().Contains(nameof(Environment.StackTrace).ToLower())
                        || line.Contains(nameof(Environment)))
                        continue;
                    else
                        return line;
                }
                return string.Empty;
            }
        }
        private static void WriteLineBase(string line)
        {
            if (WritingEnabled)
                System.Diagnostics.Debug.WriteLine(line);
        }
        private static void WriteLine(string line)
        {
            WriteLine(line, 0);
        }
        private static void WriteLine(string line, int indent)
        {
            var tabs = new StringBuilder();
            for (int i = 0; i < indent; i++)
            {
                tabs.Append(". ");
            }
            tabs.Append(line);
            WriteLineBase(FormatLine(tabs.ToString()));
        }
        private static void WriteLineIf(bool condition, string line)
        {
            if (condition)
                WriteLine(line, 0);
        }
        private static void WriteLineIf(bool condition, string line, int indent)
        {
            if (condition)
                WriteLine(line, indent);
        }
        private static string ToTimeStringWithMilliseconds(this DateTime dateTime)
        {
            return dateTime.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);
        }
        private static string FormatLine(string line)
        {
            return $"####  [{DateTime.Now.ToTimeStringWithMilliseconds()}]: {line}  #### :    {GetStackTraceLine()}";
        }
    }

И, на всякий, код примера:

    public class TreeItem : IWithBreakpoints
    {
        public string Name { get; } = Guid.NewGuid().ToString().Substring(1,4);
        public TreeItem Parent { get; set; }
        public List<TreeItem> Children { get; } = new List<TreeItem>();
        public TreeItem(TreeItem parent, int childrenCount)
        {
            NestingLevel = parent != null ? parent.NestingLevel + 1 : 0;
            if (this.Debug())
                Debugger.Break();
            Parent = parent ?? null;
            if (childrenCount == 0)
                return;
            for (int i = 0; i <= childrenCount-1; i++ )
            {
                Children.Add(new TreeItem(this, childrenCount - 1));
            }
            if (this.Debug())
                Debugger.Break();
        }
        public void Traverse(Action<TreeItem> action)
        {
            if (this.Debug())
                Debugger.Break();
            action(this);
            foreach(var child in this.Children)
            {
                child.Traverse(action);
            }
        }
        //  --------------------------
        public bool EnableBreakpoints { get; set; } = true;
        public int NestingLevel { get; set; }
        public string GetDebugInfo()
        {
            return $"{Name} (ChildrenCount={Children.Count}): ";
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Breakpoints.Enabled = true;
            Breakpoints.WritingEnabled = true;
            Debug.WriteLine("------------------------------------");
            var item = new TreeItem(null, 3);
            Debug.WriteLine("------------------------------------");
            item.Traverse(it => Console.WriteLine(it.Name));
            Console.ReadLine();
        }
    }

Updates:

Upd1:

  • уточняю, что в данном случае вывод в итоге происходит в окно студии output: System.Diagnostics.Debug.WriteLine(line).
  • пришло еще пару мелких идей по совершенствованию, например: можно было бы вместо того, чтобы для каждого объекта прописывать IsEnabled, чтобы у таких объектов был ключ, а потом по ключу прописывать (или хранить) enabled/disabled
  • потом прочитал, что лучше использовать pattern-ы decorator или visitor, спасибо за совет!
  • еще то, что методу this.Debug() можно было бы сделать перегрузку this.Debug(string debugInfo), которая бы вместо стандартной строки объекта писала бы конкретное сообщение
  • еще теоретически можно как-то ловить информацию полного stack-trace по каждой такой точке останова, но тогда нужен какой-то tool.
Answer 2

Лично я не представляю себе, где бы мне в реальных сценариях понадобилась глубокая интеграция отладки в код приложения. Кому она может понадобиться? Пользователю у которого на компьютере нет студии? Нужно просто писать приложение хорошо и модульно, не забывать писать юнит-тесты из которых вполне себе доступна не только кнопка Run, но и Debug - и можно провалиться внутрь исполняемого кода и посмотреть вживую, что там происходит.

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

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

В таком случае мне вполне достаточно добавить одну строку в начале скрипта:

//Debugger.Launch();
Answer 3

Из моего другого ответа можно выбрать только метод WriteLine(string), который сам добавляет в конце строчки имя класса и метода - инфу из StackTrace-a (так же выбрать сопутствующие другие методы).

Далее поверх вызова этого WriteLine поставить обыкновенную студийную точку останова.

Тогда и получается реализация задумки, то есть, при остановке в отладке можно посмотреть StackTrace, а так же по логам в output тоже что-то можно посмотреть.

READ ALSO
c# компиляция проекта без visual studio

c# компиляция проекта без visual studio

как компилировать программу wpf без студии? пишу в консоли:

104
502 bad gateway donationalerts

502 bad gateway donationalerts

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

150
Как поднять дные выше и вывести их PHP 7.1

Как поднять дные выше и вывести их PHP 7.1

Всем привет, прошу помочь в решение очень сложного вопроса (Мучаюсь с ним уже 4 дня)

83
Data Transfer Object: как инициализировать и должны ли быть геттеры?

Data Transfer Object: как инициализировать и должны ли быть геттеры?

Недавно узнал о паттерне DTO, до этого в приложении мои методы возвращали массивы - и это был ад их рефакторитьТеперь понял, что этот паттерн...

74