Visual Studio 调试器指南---自定义调试视图
在 Visual Studio 调试器中创建数据的自定义视图C++/C#
调试器提供了许多用于检查和修改程序状态的工具。 这些工具中的大多数仅在中断模式下有效。
在变量窗口和数据提示中创建数据的自定义视图
许多调试器窗口(如 "自动" 和 "监视" 窗口)都允许您检查变量。 您可以自定义C++类型、托管对象以及您自己的类型在调试器变量窗口和数据提示中的显示方式。
创建自定义可视化工具
可视化工具使您能够以有意义的方式查看对象或变量的内容。 在 Visual Studio 调试器中,可视化工具是指可以使用放大镜图标打开的其他窗口。 例如,HTML 可视化工具显示 HTML 字符串如何在浏览器中进行解释和显示。 你可以通过数据提示、"监视" 窗口 、"自动" 窗口和 "局部变量" 窗口访问可视化工具。 "快速监视" 对话框还提供可视化工具。
自定义C++视图
使用 Natvis 框架在C++调试器中创建对象的自定义视图
Visual Studio Natvis 框架可以自定义本机类型在调试器变量窗口(例如局部变量、监视以及数据提示窗口)中显示的方式。 Natvis 的可视化功能可以让你创建的类型在调试期间更加直观清晰。
Natvis 替换了 Visual Studio 早期版本中的 autoexp.dat 文件,提供了 XML 语法、更好的诊断功能、版本控制功能以及多文件支持功能。
Natvis 可视化效果
你可以使用 Natvis 框架为自己创建的类型创建可视化规则,让开发人员在调试过程中更轻松地查看这些类型。
例如,下图显示的类型 Windows::UI::Xaml::Controls::TextBox 的变量在调试器窗口中未应用任何自定义可视化。
突出显示的行显示 Text
类的 TextBox
属性。 由于类的层次结构很复杂,因此很难找到这个属性。 调试器不知道如何解释自定义字符串类型,所以你看不到文本框中的字符串。
如果应用了 Natvis 自定义可视化工具规则,那么在变量窗口中,同样的TextBox
看起来就简单得多。 类的重要成员会显示在一起,并且调试器会显示自定义字符串类型的基础字符串值。
在 C++ 项目中使用 .natvis 文件
Natvis 使用Natvis文件来指定可视化规则。 Natvis文件是具有NATVIS扩展名的 XML 文件。 Natvis 架构在 %VSINSTALLDIR%\Xml\Schemas\natvis.xsd中定义。
.natvis 文件的基本结构由一个或多个代表可视化条目的 Type
元素构成。 每个 Type
元素的完全限定名称都在其 Name
属性中指定。
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="MyNamespace::CFoo">
.
.
</Type>
<Type Name="...">
.
.
</Type>
</AutoVisualizer>
Visual Studio 在 %VSINSTALLDIR%\Common7\Packages\Debugger\Visualizers文件夹中提供了一些natvis文件。 这些文件包含许多通用类型的可视化规则,并且可用作编写新类型的可视化效果的示例。
将 natvis 文件添加到C++项目
可以将natvis文件添加到任何C++项目。
若要添加新的natvis文件:
-
在解决方案资源管理器C++中选择项目节点,然后选择 "项目 > "添加新项",或右键单击该项目并选择"添加 > 新项"。
-
在 "添加新项" 对话框中,选择 " Visual C++ > 实用工具" > 调试器可视化文件(. natvis) 。
-
命名该文件,然后选择 "添加"。
新文件将添加到解决方案资源管理器,并在 Visual Studio 文档窗格中打开。
Visual Studio 调试器会自动在 C++ 项目中加载 .natvis 文件,默认情况下,还会在生成项目时,在 .pdb 文件中包含它们。 在调试生成的应用时,调试器会从 .pdb 文件加载 .natvis 文件,即使你没有打开该项目也是如此。 如果不想在 .pdb 中包含 .natvis 文件,可以从生成的 .pdb 文件中将其排除。
从 .pdb 中排除 .natvis 文件:
-
在解决方案资源管理器中选择 .natvis 文件,然后选择属性图标,或右键单击该文件并选择属性。
-
按下从生成中排除旁边的箭头并选择是,然后选择确定。
如果是调试可执行的项目,则使用解决方案项来添加 .pdb 中不包含的 .natvis 文件,因为没有可用的 C++ 项目。从 .pdb 加载的 Natvis 规则仅适用于 .pdb 引用的模块中的类型。 例如,如果 Module1.pdb 包含一个名为 Test
的类型的 Natvis 条目,则它仅适用于 Test
Module1.dll中的 类。 如果另一个模块也定义了一个名为 Test
的类,则 Module1.pdb 的 Natvis 条目不适用于它。
通过 VSIX 包安装并注册natvis文件:
VSIX 包可以安装和注册natvis文件。 无论他们安装在何处,都将在调试过程中自动提取所有注册的natvis文件。
-
将natvis文件包括在 VSIX 包中。 例如,对于以下项目文件:
XML
-
<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0"> <ItemGroup> <VSIXSourceItem Include="Visualizer.natvis" /> </ItemGroup> </Project>
-
在source.extension.vsixmanifest文件中注册natvis文件:
XML
-
<?xml version="1.0" encoding="utf-8"?> <PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011"> <Assets> <Asset Type="NativeVisualizer" Path="Visualizer.natvis" /> </Assets> </PackageManifest>
Natvis 文件位置
如果你希望将 .natvis 文件应用于多个项目,可以将它们添加到用户目录或系统目录中。
将按照以下顺序来评估 .Natvis 文件:
-
所调试的 .pdb 文件中内嵌的所有的 .natvis 文件,除非加载的项目中存在同名文件。
-
加载的 C++ 项目或顶级解决方案中的所有 .natvis 文件。 这包括所有已加载的 C++ 项目(包括类库),但不包括其他语言的项目。
-
通过 VSIX 包安装并注册的任何natvis文件。
- 特定于用户的 Natvis 目录 (例如, %USERPROFILE%\Documents\Visual Studio 2019\Visualizers)。
- 系统级 Natvis 目录 ( %VSINSTALLDIR%\Common7\Packages\Debugger\Visualizers)。 此目录中包含随 Visual Studio 一起安装的 .natvis 文件。 如果你具有管理员权限,可以将文件添加到此目录中。
调试时修改 natvis 文件
你可以在调试项目时,在 IDE 中修改 .natvis 文件。 在用于调试的同一个 Visual Studio 实例中打开该文件,进行修改,然后保存。 保存后,监视和局部变量窗口就会随之更新,显示所做的更改。你还可以在调试的解决方案中添加或删除 .natvis 文件,Visual Studio 会随之添加或删除相关的可视化效果。在调试时,无法更新 .pdb 文件中内嵌的 .natvis 文件。
如果你在 Visual Studio 以外修改 .natvis 文件,所做的更改不会自动生效。 若要更新调试器窗口,可以在监视窗口中重新计算 .natvisreload 命令。 更改随后就会生效,无需重新启动调试会话。
还可以使用 .natvisreload 命令将 .natvis 文件升级到较新的版本。 例如, .natvis 文件可能被纳入了源代码管理中,并且你需要获取其他人最近所做的更改。
表达式和格式化
Natvis 的可视化功能使用 C++ 表达式来指定要显示的数据项。 除了上下文运算符 (C++) 中所述的、调试器中的 C++ 表达式的增强功能和限制外,还要注意以下事项:
-
Natvis 表达式在可视化对象上下文而非当前堆栈框架中进行计算。 例如,Natvis 表达式中的
x
指的是可视化对象中名为x的字段,而不是当前函数中名为x的局部变量。 尽管可以访问全局变量,但不能访问 Natvis 表达式中的局部变量。 -
Natvis 表达式不允许函数计算或副作用。 函数调用和赋值运算符会被忽略。 由于调试器内部函数没有副作用,因此可以从任何 Natvis 表达式随意调用,即使系统不允许进行其他函数调用也是如此。
-
要控制表达式的显示方式,可以使用 C++ 中的格式说明符中所述的任何格式说明符。 如果条目由 Natvis 在内部使用,格式说明符将被忽略,例如
Size
ArrayItems 扩展中的 表达式。
Natvis 视图
你可以定义不同的 Natvis 视图,以便用不同的方式来显示类型。 例如,下面是 std::vector
的一个可视化效果,定义了一个名为 simple
的简化视图。 默认视图和 DisplayString
视图中都显示了 ArrayItems
和 simple
元素,但 [size]
视图中却未显示 [capacity]
和 simple
项。
<Type Name="std::vector<*>">
<DisplayString>{{ size={_Mylast - _Myfirst} }}</DisplayString>
<Expand>
<Item Name="[size]" ExcludeView="simple">_Mylast - _Myfirst</Item>
<Item Name="[capacity]" ExcludeView="simple">_Myend - _Myfirst</Item>
<ArrayItems>
<Size>_Mylast - _Myfirst</Size>
<ValuePointer>_Myfirst</ValuePointer>
</ArrayItems>
</Expand>
</Type>
在监视窗口中,使用 ,view 格式说明符来指定替代视图。 simple 视图将显示为 vec,view(simple) :
Natvis 错误
当调试器遇到可视化条目中的错误时,会忽略它们。 要么以原始格式显示类型,要么选择其他合适的可视化效果。 你可以使用 Natvis 的诊断功能来了解调试器忽略可视化条目的原因,还可以查看基础语法和分析错误。
若要启用 Natvis 诊断:
- 在 "工具" > 选项"(或"调试 > 选项") >调试 > 输出窗口中,将" Natvis 诊断消息(C++仅) "设置为"错误"、"警告"或 " 详细"
错误将显示在 "输出" 窗口中。
Visual C/C++自定义可视化工具兼容性
从 Visual Studio 2019 开始, C++包含一个改进的调试器,它使用外部64位进程来承载其内存密集型组件。 作为此更新的一部分,必须更新 C/C++ expression 计算器的某些扩展以使它们与新调试器兼容。
如果你当前使用的是旧的 CC++ /EE 外接程序C++或 c/自定义可视化工具,则可以通过转到 "工具" > 选项" > 调试",然后取消选择 "加载调试" 来关闭此外部进程的使用情况。外部进程中的符号(仅限本机) 。 如果取消选择此选项,将会大幅增加 IDE (devenv)进程中的内存使用率。 因此,如果您希望调试大型项目,则建议您与扩展的所有者合作,使其与此调试选项兼容。
托管代码自定义视图
创建托管对象的自定义视图
可以在调试器变量窗口中自定义 Visual Studio 显示数据类型的方式。在C#、Visual Basic、 F#和C++ (C++仅限/cli 代码)中,你可以使用 DebuggerTypeProxyAttribute、DebuggerDisplayAttribute 和 DebuggerBrowsableAttribute 为自定义数据添加扩展。
在 .NET Framework 2.0 代码中,Visual Basic 不支持 DebuggerBrowsable 属性。 此限制已在最新版本的 .NET 中删除。
使用 DebuggerDisplay 特性(C#、Visual Basic、 F#、 C++/cli)告诉调试器要显示的内容
DebuggerDisplayAttribute 控制对象、属性或字段在调试器变量窗口中的显示方式。 此特性可应用于类型、委托、属性、字段和程序集。 如果应用于基类型,则属性也适用于子类。DebuggerDisplay
特性有一个参数,此参数是要在值列中为类型的实例显示的字符串。 此字符串可以包含大括号({
和 }
)。 一对大括号之间的文本将作为字段、属性或方法进行计算。
如果一个类中有重写的 ToString()
方法,调试器将使用该重写的方法而非默认 {<typeName>}
。 因此,如果你已重写 ToString()
方法,调试器将使用重写的方法而非默认{<typeName>}
,你无需使用 DebuggerDisplay
。 如果同时使用,DebuggerDisplay
属性优先于替代的 ToString()
方法。 @No__t_0 特性还优先于子类中重写的 ToString()
方法。
调试器是否计算此隐式 ToString()
调用取决于“工具”/“选项”/“调试” 对话框中的用户设置。 Visual Basic 不实现此隐式 ToString()
计算。
如果在“工具”/“选项”/“调试” 对话框中选中了“在变量窗口中显示对象的原始结构” 复选框,则将忽略 DebuggerDisplay
特性。对于本机代码,此属性仅在/Cli 代码C++中受支持。
下表显示 DebuggerDisplay
特性的一些可能用法和示例输出。
特性 | 显示在“值”列中的输出 |
---|---|
[DebuggerDisplay("x = {x} y = {y}")] 在具有 x 和 y 字段的类型上使用。 |
x = 5 y = 18 |
[DebuggerDisplay("String value is {getString()}")] 参数语法在不同的语言中会有所不同。 因此,使用时要小心。 |
String value is [5, 6, 6] |
DebuggerDisplay
还可以接受命名参数。
参数 | 目标 |
---|---|
Name ,Type |
这些参数影响变量窗口的 “名称” 和 “类型” 列。 (可将它们设置为使用与构造函数相同的语法的字符串。)如果过度使用这些参数或使用这些参数不当,则会导致混乱的输出。 |
Target ,TargetTypeName |
指定在程序集级别使用该特性时的目标类型。 |
autoexp.cs 文件在程序集级别使用 DebuggerDisplay 特性。 autoexp.cs 文件确定 Visual Studio 用于 .NET 对象的默认扩展。 可以检查 autoexp.cs 文件以获得如何使用 DebuggerDisplay 特性的示例,也可以修改和编译 autoexp.cs 文件以更改默认扩展。 在修改 autoexp.cs 文件之前,一定要对该文件进行备份。
若要生成 autoexp.cs,请打开 VS2015 开发人员命令提示,并运行以下命令
cd <directory containing autoexp.cs>
csc /t:library autoexp.cs
将在下一调试会话中选取对 autoexp.dll 的更改。
在 DebuggerDisplay 中使用表达式
虽然您可以在 DebuggerDisplay 特性中的大括号之间使用常规表达式,但建议不要这样做。
DebuggerDisplay 中的常规表达式只能隐式访问目标类型的当前实例的 this
指针。 该表达式不能访问别名、局部变量或指针。 如果表达式引用属性,则不处理这些属性上的特性。 例如,如果字段 [DebuggerDisplay("Object {count - 2}")]
是 8,则 C# 代码 Object 6
将显示 count
。
在 DebuggerDisplay 中使用表达式可能导致以下问题:
-
计算表达式是调试器中最消耗资源的操作,并且表达式在每次显示时都会被计算。 在单步执行代码时,这可能会导致性能问题。 例如,当元素的数量很大时,一个用来在集合或列表中显示值的复杂表达式可能会很慢。
-
表达式是通过当前堆栈帧的语言表达式计算器计算的,而不是通过表达式记录语言的计算器计算的。 当语言不同时,这可能导致不可预知的结果。
-
计算表达式可更改应用程序的状态。 例如,设置属性值的表达式在执行代码时会改变属性值。
减少表达式计算可能出现的问题的一种方法是创建执行操作并返回字符串的私有属性。 然后,DebuggerDisplay 特性可以显示该私有属性的值。 以下示例实现了这种模式:
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class MyClass
{
public int count { get; set; }
public bool flag { get; set; }
private string DebuggerDisplay
{
get
{
return string.Format("Object {0}", count - 2);
}
}
}
",Nq" 后缀通知表达式计算器在显示最终值时删除引号(nq = no 引号)。
示例
下面的代码示例演示如何使用 DebuggerDisplay
以及 DebuggerBrowseable
和 DebuggerTypeProxy
。 在调试器变量窗口(如 “监视” 窗口)中查看时,它生成类似以下内容的扩展:
名称 | “值” | Type |
---|---|---|
项 | "three" | object {string} |
“值” | 3 | object {int} |
[DebuggerDisplay("{value}", Name = "{key}")]
internal class KeyValuePairs
{
private IDictionary dictionary;
private object key;
private object value;
public KeyValuePairs(IDictionary dictionary, object key, object value)
{
this.value = value;
this.key = key;
this.dictionary = dictionary;
}
public object Key
{
get { return key; }
set
{
object tempValue = dictionary[key];
dictionary.Remove(key);
key = value;
dictionary.Add(key, tempValue);
}
}
public object Value
{
get { return this.value; }
set
{
this.value = value;
dictionary[key] = this.value;
}
}
}
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[DebuggerTypeProxy(typeof(HashtableDebugView))]
class MyHashtable
{
public Hashtable hashtable;
public MyHashtable()
{
hashtable = new Hashtable();
}
private string DebuggerDisplay { get { return "Count = " + hashtable.Count; } }
private class HashtableDebugView
{
private MyHashtable myhashtable;
public HashtableDebugView(MyHashtable myhashtable)
{
this.myhashtable = myhashtable;
}
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePairs[] Keys
{
get
{
KeyValuePairs[] keys = new KeyValuePairs[myhashtable.hashtable.Count];
int i = 0;
foreach (object key in myhashtable.hashtable.Keys)
{
keys[i] = new KeyValuePairs(myhashtable.hashtable, key, myhashtable.hashtable[key]);
i++;
}
return keys;
}
}
}
}
使用 DebuggerTypeProxy 特性(C#Visual Basic, C++/cli)告诉调试器要显示的类型
DebuggerTypeProxyAttribute 指定类型的代理或替身,并更改类型在调试器窗口中的显示方式。 查看具有代理的变量时,代理将代替“显示”中的原始类型。 调试器变量窗口仅显示代理类型的公共成员。 不会显示私有成员。
此特性可应用于:
- 结构
- 类
- 程序集
备注
对于本机代码,此属性仅在/Cli 代码C++中受支持。
类型代理类必须具有一个构造函数,该函数采用代理将替换的类型的参数。 在每次需要显示目标类型的变量时,调试器都会创建类型代理类的一个新实例。 这会对性能产生一定影响。 因此,不应在构造函数中执行非必需的工作。
若要最大程度地减小性能损失,表达式计算器将不检查类型的显示代理上的特性,除非用户在调试器窗口中单击 + 符号或使用 DebuggerBrowsableAttribute 扩展该类型。 因此,不应将特性置于显示类型自身中。 特性可以且应该用于显示类型的正文中。
类型代理最好是作为特性目标类中的私有嵌套类。 这样,它便能轻松访问内部成员。
可以继承 DebuggerTypeProxyAttribute,因此,如果在基类上指定了类型代理,则它将应用于任何派生类,除非这些派生类指定其自己的类型代理。
如果在程序集级别使用 DebuggerTypeProxyAttribute,则 Target
参数将指定代理要替换的类型。
有关如何将此属性与 DebuggerDisplayAttribute 和 DebuggerTypeProxyAttribute 一起使用的示例,请参阅使用 DebuggerDisplay 属性。
将泛型与 DebuggerTypeProxy 一起使用
对泛型的支持是有限的。 对于 C#,DebuggerTypeProxy
只支持开放类型。 开放类型(也称作“非构造类型”)是一种还未使用其类型参数的自变量实例化的泛型类型。 不支持封闭类型(也称作“构造类型”)。
开放类型的语法类似于:
Namespace.TypeName<,>
如果使用泛型类型作为 DebuggerTypeProxy
中的目标,则必须使用该语法。 DebuggerTypeProxy
机制将为你推理类型参数。
有关中C#的 C# 打开和关闭类型的详细信息,请参阅语言规范部分20.5.2 打开和关闭类型。
Visual Basic 没有开放类型语法,因此您无法在 Visual Basic 中执行同样的操作。 而必须使用开放类型名称的字符串表示形式。
"Namespace.TypeName'2"