当前位置: 首页 > news >正文

C#/.NET 调试的时候显示自定义的调试信息(DebuggerDisplay 和 DebuggerTypeProxy)

使用 Visual Studio 调试 .NET 程序的时候,在局部变量窗格或者用鼠标划到变量上就能查看变量的各个字段和属性的值。默认显示的是对象 ToString() 方法调用之后返回的字符串,不过如果 ToString() 已经被占作它用,或者我们只是希望在调试的时候得到我们最希望关心的信息,则需要使用 .NET 中调试器相关的特性。

本文介绍使用 DebuggerDisplayAttributeDebuggerTypeProxyAttribute 来自定义调试信息的显示。(同时隐藏我们在背后做的这些见不得人的事儿。)


本文内容

    • 示例代码
    • DebuggerDisplay
    • DebuggerTypeProxy
    • 最终代码

示例代码

比如我们有一个名为 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

不过,展开对象查看的时候可以看到一个 DebuggerDisplay 的属性,而这个属性我们只是调试使用,这是个垃圾属性,并不应该影响我们的查看。

里面有一个 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 类型上。

这样,最终的显示效果是这样的:

使用 DebuggerTypeProxy

点击 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();
        }
    }
}

参考资料

  • DebuggerTypeProxyAttribute Class (System.Diagnostics) - Microsoft Docs
  • DebuggerDisplayAttribute Class (System.Diagnostics) - Microsoft Docs
  • Using DebuggerTypeProxy Attribute - Visual Studio - Microsoft Docs
  • Using the DebuggerDisplay Attribute - Visual Studio - Microsoft Docs

我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

相关文章:

  • 详解 .NET 反射中的 BindingFlags 以及常用的 BindingFlags 使用方式
  • 在 csproj 文件中使用系统环境变量的值(示例将 dll 生成到 AppData 目录下)
  • git subtree 的使用
  • 让你的 VSCode 具备调试 C# 语言 .NET Core 程序的能力
  • 手工编辑 tasks.json 和 launch.json,让你的 VSCode 具备调试 .NET Core 程序的能力
  • C#/.NET 如何结束掉一个进程
  • C#/.NET 移动或重命名一个文件夹(如果存在,则合并而不是出现异常报错)
  • 如何创建应用程序清单文件 App.Manifest,如何创建不带清单的应用程序
  • 应用程序清单 Manifest 中各种 UAC 权限级别的含义和效果
  • 启用 Windows 审核模式(Audit Mode),以 Administrator 账户来设置电脑的开箱体验
  • Windows 中的 UAC 用户账户控制
  • Windows 下使用 runas 命令以指定的权限启动一个进程(非管理员、管理员)
  • Windows 的 UAC 设置中的通知等级实际上只有两个档而已
  • Windows 系统上使用任务管理器查看进程的各项属性(命令行、DPI、管理员权限等)
  • C#/.NET 如何获取一个异常(Exception)的关键特征,用来判断两个异常是否表示同一个异常
  • [js高手之路]搞清楚面向对象,必须要理解对象在创建过程中的内存表示
  • 【个人向】《HTTP图解》阅后小结
  • 【面试系列】之二:关于js原型
  • ES6核心特性
  • jquery ajax学习笔记
  • Less 日常用法
  • Theano - 导数
  • uni-app项目数字滚动
  • vue脚手架vue-cli
  • 从零开始的无人驾驶 1
  • 分享一个自己写的基于canvas的原生js图片爆炸插件
  • 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
  • 力扣(LeetCode)56
  • 强力优化Rancher k8s中国区的使用体验
  • 为物联网而生:高性能时间序列数据库HiTSDB商业化首发!
  • 【云吞铺子】性能抖动剖析(二)
  • ​一些不规范的GTID使用场景
  • #pragma once与条件编译
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • (C++)栈的链式存储结构(出栈、入栈、判空、遍历、销毁)(数据结构与算法)
  • (pojstep1.1.1)poj 1298(直叙式模拟)
  • (SpringBoot)第七章:SpringBoot日志文件
  • (超详细)语音信号处理之特征提取
  • (分享)一个图片添加水印的小demo的页面,可自定义样式
  • (附源码)python房屋租赁管理系统 毕业设计 745613
  • (篇九)MySQL常用内置函数
  • (未解决)macOS matplotlib 中文是方框
  • (转)Linux NTP配置详解 (Network Time Protocol)
  • ***检测工具之RKHunter AIDE
  • **python多态
  • .net 4.0发布后不能正常显示图片问题
  • .net core 调用c dll_用C++生成一个简单的DLL文件VS2008
  • .NET core 自定义过滤器 Filter 实现webapi RestFul 统一接口数据返回格式
  • .NET 设计模式初探
  • .net 使用$.ajax实现从前台调用后台方法(包含静态方法和非静态方法调用)
  • .NET国产化改造探索(一)、VMware安装银河麒麟
  • /proc/stat文件详解(翻译)
  • /usr/bin/python: can't decompress data; zlib not available 的异常处理
  • @我的前任是个极品 微博分析
  • [ 隧道技术 ] 反弹shell的集中常见方式(二)bash反弹shell