C#/.NET 調試的時候顯示自定義的調試資訊(DebuggerDisplay 和 DebuggerTypeProxy)

  • 2020 年 2 月 10 日
  • 筆記

使用 Visual Studio 調試 .NET 程式的時候,在局部變數窗格或者用滑鼠划到變數上就能查看變數的各個欄位和屬性的值。默認顯示的是對象 ToString() 方法調用之後返回的字元串,不過如果 ToString() 已經被占作它用,或者我們只是希望在調試的時候得到我們最希望關心的資訊,則需要使用 .NET 中調試器相關的特性。

本文介紹使用 DebuggerDisplayAttributeDebuggerTypeProxyAttribute 來自定義調試資訊的顯示。(同時隱藏我們在背後做的這些見不得人的事兒。)


示例程式碼

比如我們有一個名為 CommandLine 的類型,表示從命令行傳入的參數;內有一個字典,包含命令行參數的所有資訊。

public class CommandLine  {      private readonly Dictionary<string, IReadOnlyList<string>> _optionArgs;      private CommandLine(Dictionary<string, IReadOnlyList<string>> optionArgs)          => _optionArgs = optionArgs ?? throw new ArgumentNullException(nameof(optionArgs));  }

現在,我們在 Visual Studio 裡面調試得到一個 CommandLine 的實例,然後使用調試器查看這個實例的屬性、欄位和集合。

然後,這樣的一個字典嵌套列表的類型,竟然需要點開 4 層才能知道命令行參數究竟是什麼。這樣的調試效率顯然是太低了!

DebuggerDisplay

使用 DebuggerDisplayAttribute 可以幫助我們直接在局部變數窗格或者滑鼠划過的時候就看到對象中我們最希望了解的資訊。

現在,我們在 CommandLine 上加上 DebuggerDisplayAttribute

// 此段程式碼非最終版本。  [DebuggerDisplay("CommandLine: {DebuggerDisplay}")]  public class CommandLine  {      private readonly Dictionary<string, IReadOnlyList<string>> _optionArgs;      private CommandLine(Dictionary<string, IReadOnlyList<string>> optionArgs)          => _optionArgs = optionArgs ?? throw new ArgumentNullException(nameof(optionArgs));        private string DebuggerDisplay => string.Join(' ', _optionArgs          .Select(pair => $"{pair.Key}{(pair.Key == null ? "" : " ")}{string.Join(' ', pair.Value)}"));  }

效果有了:

不過,展開對象查看的時候可以看到一個 DebuggerDisplay 的屬性,而這個屬性我們只是調試使用,這是個垃圾屬性,並不應該影響我們的查看。

我們使用 DebuggerBrowsable 特性可以關閉某個屬性或者欄位在調試器中的顯示。於是程式碼可以改進為:

--  [DebuggerDisplay("CommandLine: {DebuggerDisplay}")]  ++  [DebuggerDisplay("CommandLine: {DebuggerDisplay,nq}")]      public class CommandLine      {          private readonly Dictionary<string, IReadOnlyList<string>> _optionArgs;          private CommandLine(Dictionary<string, IReadOnlyList<string>> optionArgs)              => _optionArgs = optionArgs ?? throw new ArgumentNullException(nameof(optionArgs));    ++      [DebuggerBrowsable(DebuggerBrowsableState.Never)]          private string DebuggerDisplay => string.Join(' ', _optionArgs              .Select(pair => $"{pair.Key}{(pair.Key == null ? "" : " ")}{string.Join(' ', pair.Value)}"));      }

添加了從不顯示此欄位(DebuggerBrowsableState.Never),在調試的時候,展開後的屬性列表裡面沒有垃圾 DebuggerDisplay 屬性了。

另外,我們在 DebuggerDisplay 特性的中括弧中加了 nq 標記(No Quote)來去掉最終顯示的引號。

DebuggerTypeProxy

雖然我們使用了 DebuggerDisplay 使得命令行參數一眼能看出來,但是看不出來我們把命令行解析成什麼樣了。於是我們需要更精細的視圖。

然而,上面展開 _optionArgs 欄位的時候,依然需要展開 4 層才能看到我們的所有資訊,所以我們使用 DebuggerTypeProxyAttribute 來優化調試器實例內部的視圖。

class CommandLineDebugView  {      [DebuggerBrowsable(DebuggerBrowsableState.Never)]      private readonly CommandLine _owner;        public CommandLineDebugView(CommandLine owner)      {          _owner = owner;      }        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]      private string[] Options => _owner._optionArgs          .Select(pair => $"{pair.Key}{(pair.Key == null ? "" : " ")}{string.Join(' ', pair.Value)}")          .ToArray();  }

我面寫了一個新的類型 CommandLineDebugView,並在構造函數中允許傳入要優化顯示的類型的實例。在這裡,我們寫一個新的 Options 屬性把原來字典裡面需要四層才能展開的值合併成一個字元串集合。

但是,我們在 Options 上標記 DebuggerBrowsableState.RootHidden

  1. 如果這是一個集合,那麼這個集合將直接顯示到調試視圖的上一級視圖中;
  2. 如果這是一個普通對象,那麼這個對象的各個屬性欄位將合併到上一級視圖中顯示。

別忘了我們還需要禁止 _owner 在調試器中顯示,然後把 [DebuggerTypeProxy(typeof(CommandLineDebugView))] 加到 CommandLine 類型上。

這樣,最終的顯示效果是這樣的:

點擊 Raw View 可以看到我們沒有使用 DebuggerTypeProxyAttribute 視圖時的屬性和欄位。

最終程式碼

using System;  using System.Collections.Generic;  using System.Diagnostics;  using System.Linq;  using Walterlv.Framework.StateMachine;    namespace Walterlv.Framework  {      [DebuggerDisplay("CommandLine: {DebuggerDisplay,nq}")]      [DebuggerTypeProxy(typeof(CommandLineDebugView))]      public class CommandLine      {          private readonly Dictionary<string, IReadOnlyList<string>> _optionArgs;          private CommandLine(Dictionary<string, IReadOnlyList<string>> optionArgs)              => _optionArgs = optionArgs ?? throw new ArgumentNullException(nameof(optionArgs));            [DebuggerBrowsable(DebuggerBrowsableState.Never)]          private string DebuggerDisplay => string.Join(' ', _optionArgs              .Select(pair => $"{pair.Key}{(pair.Key == null ? "" : " ")}{string.Join(' ', pair.Value)}"));            private class CommandLineDebugView          {              [DebuggerBrowsable(DebuggerBrowsableState.Never)]              private readonly CommandLine _owner;                public CommandLineDebugView(CommandLine owner) => _owner = owner;                [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]              private string[] Options => _owner._optionArgs                  .Select(pair => $"{pair.Key}{(pair.Key == null ? "" : " ")}{string.Join(' ', pair.Value)}")                  .ToArray();          }      }  }

參考資料

本文會經常更新,請閱讀原文: https://blog.walterlv.com/post/display-instance-info-in-custom-debugger-view.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。

本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名 呂毅 (包含鏈接: https://blog.walterlv.com ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發布。如有任何疑問,請 與我聯繫 ([email protected])