将 .NET 类转储到调试输出






4.92/5 (29投票s)
将 .NET 类转换为易于阅读的调试输出,无需过多努力

引言
一个老生常谈的问题:如何在代码执行期间找到变量值? 带有断点和监视窗口的调试器非常棒,但前提是程序可以在正确的时间在正确的位置停止。 遗憾的是,条件并不总是那么理想,将所需的值转储到日志文件或控制台以供将来分析是一个更现实的选择。
另一个常见的场景是一个简单的调试或一次性使用的实用程序,它需要生成人类可读的输出,并且代码量最少。 这种工具并非针对非技术用户,美化输出也不是优先事项,但花在生成有用输出上的时间是必要的。
在任何一种情况下,我经常发现自己编写如下代码
class C {
public int X;
public string Y;
public DirectoryInfo Child;
public int Z;
}
...
C c=...
// Oops, that prints just "MyNamespace.C", hardly useful
// Debug.WriteLine("c="+c);
Debug.WriteLine("--- Value of c ----");
Debug.WriteLine("c.X="+c.X);
Debug.WriteLine("c.Y="+c.X); // Oops, dumping the wrong field
Debug.WriteLine("c.Child=FileInfo
{ Name="+c.Child.Name +"}"); // This will sometimes throw NullReference exception
Debug.WriteLine("-------"); // Forgot to dump Z field :(
这不仅冗长且编写起来很烦人,而且此类代码的第一次迭代通常无用。 要么被转储的对象没有重写其 ToString
方法,要么由于拼写错误导致输出混乱,要么未包含有用的字段,要么抛出异常,要么多个线程同时执行代码,并且 Debug.WriteLine
输出混合在一起。 当属性中存在匿名对象、数组、枚举、列表、对其他对象的引用(带循环)时,情况会变得更糟,也需要将其放入日志文件中。
有一种更好的方法:一种通用的调试写入器,它使用反射来显示所有对象字段和属性,如有必要,遍历对象图,并生成一些人类可读且包含足够信息以供分析的内容。 这几乎不是一个新想法,CodeProject 已经有 一篇关于该主题的文章,并且可以在 Internet 上找到一些其他解决方案,但我想要一些更灵活、更简单、更通用并且是一个单一的 .cs 文件,没有任何依赖关系,因此可以添加到任何项目中。 在开发 XSharper 框架/脚本语言 期间,创建了 Dump 类。
Using the Code
将 Dump.cs 文件包含到您的项目中,或链接完整的 XSharper.Core 程序集。 现在,上面有问题的转储代码可以替换为
Debug.WriteLine(Dump.ToDump(c),"c")
以生成
c = (C) { /* #1, 03553390 */
X = (int) 5 (0x5)
Y = (string) "Hello"
Child = (DirectoryInfo) { /* #2, 01fed012 */
Name = (string) "C:\"
Parent = (DirectoryInfo) "<null>" /* ToString */
Exists = (bool) true
Root = (DirectoryInfo) "C:\" /* ToString */
FullName = (string) "C:\"
Extension = (string) ""
CreationTime = (DateTime) 2009-03-03T08:30:17.8593750-05:00 (Local)
CreationTimeUtc = (DateTime) 2009-03-03T13:30:17.8593750Z (Utc)
LastAccessTime = (DateTime) 2010-04-14T00:38:01.6864128-04:00 (Local)
LastAccessTimeUtc = (DateTime) 2010-04-14T04:38:01.6864128Z (Utc)
LastWriteTime = (DateTime) 2010-04-13T12:22:52.3763914-04:00 (Local)
LastWriteTimeUtc = (DateTime) 2010-04-13T16:22:52.3763914Z (Utc)
Attributes = (FileAttributes) [Hidden, System, Directory] /* 0x00000016 */
}
Z = (int) 0 (0x0)
}
对象 ID 和哈希码也会输出,以简化对具有循环的复杂对象图的跟踪。 例如,上面的 /* #2, 01fed012 */ 意味着对象 #2,其 GetHashCode()=0x01fed012。
还可以控制转储对象树的深度、显示的数组元素的最大数量以及其他一些小东西。 有关详细信息,请参阅 源代码 和附带的示例。
特殊类型和属性
某些类具有具有副作用的属性。 例如,检索时间很长或会使对象内部状态失效的属性。 为了防止 Dump
访问此类属性,应使用 Dump.AddHiddenProperty() static
方法注册这些属性。
此外,某些属性和类型在正常转储时会导致太多膨胀
- “膨胀”类型是指对正在调试的应用程序不重要的类型。 例如,
System.Type
、System.Reflection.Assembly
和许多其他运行时类具有数十个public internal
属性,这些属性对大多数应用程序开发人员来说几乎没有什么用处。 - “膨胀”属性在访问时会默默地创建父对象的副本。
System.IO.DirectoryInfo
类的Parent
和Root
属性就是这种设计的一个例子。 如果不进行特殊处理,转储new DirectoryInfo("C:\")
将生成一个长长的嵌套对象树,因为 "C:\" 的Root
属性返回它自己的一个新副本。
为了避免这些问题,使用 ToString()
方法将一些已知的“膨胀”类型和属性写入转储,并在输出中显示 /* ToString */。 可以通过 Dump.AddBloatProperty()
和 Dump.AddBloatType()
方法注册额外的“膨胀”类型和属性。
最好在程序启动时注册所有已知的特殊类型/属性,以避免线程问题。
另一个例子
将有关已安装 CDROM 驱动器的详细信息转储到控制台
Console.WriteLine(Dump.ToDump(
from d in DriveInfo.GetDrives()
where d.DriveType==DriveType.CDRom
select d));
//----------------------------------
(WhereArrayIterator) { /* #1, 02b89eaa */
[0] = (DriveInfo) { /* #2, 0273e403 */
Name = (string) "D:\"
DriveType = (DriveType) [CDRom] /* 0x00000005 */
DriveFormat = (string) "UDF"
IsReady = (bool) true
AvailableFreeSpace = (long) 0 (0x0)
TotalFreeSpace = (long) 0 (0x0)
TotalSize = (long) 6394689536 (0x17d273800)
RootDirectory = (DirectoryInfo) { /* #3, 025f14a9 */
Name = (string) "D:\"
Parent = (DirectoryInfo) "" /* ToString */
Exists = (bool) true
Root = (DirectoryInfo) "D:\" /* ToString */
FullName = (string) "D:\"
Extension = (string) ""
CreationTime = (DateTime) 2008-04-30T16:36:29.2960000-04:00 (Local)
CreationTimeUtc = (DateTime) 2008-04-30T20:36:29.2960000Z (Utc)
LastAccessTime = (DateTime) 2008-04-30T16:43:12.8430000-04:00 (Local)
LastAccessTimeUtc = (DateTime) 2008-04-30T20:43:12.8430000Z (Utc)
LastWriteTime = (DateTime) 2008-04-30T16:43:09.2810000-04:00 (Local)
LastWriteTimeUtc = (DateTime) 2008-04-30T20:43:09.2810000Z (Utc)
Attributes = (FileAttributes) [ReadOnly, Directory] /* 0x00000011 */
}
VolumeLabel = (string) "OS_4455.01"
}
[1] = (DriveInfo) { /* #4, 02b6a1ca */
Name = (string) "V:\"
DriveType = (DriveType) [CDRom] /* 0x00000005 */
DriveFormat = (string) "UDF"
IsReady = (bool) true
AvailableFreeSpace = (long) 0 (0x0)
TotalFreeSpace = (long) 0 (0x0)
TotalSize = (long) 2501894144 (0x951fe000)
RootDirectory = (DirectoryInfo) { /* #5, 021a7086 */
Name = (string) "V:\"
Parent = (DirectoryInfo) "" /* ToString */
Exists = (bool) true
Root = (DirectoryInfo) "V:\" /* ToString */
FullName = (string) "V:\"
Extension = (string) ""
CreationTime = (DateTime) 2009-07-14T05:26:40.0000000-04:00 (Local)
CreationTimeUtc = (DateTime) 2009-07-14T09:26:40.0000000Z (Utc)
LastAccessTime = (DateTime) 2009-07-14T05:26:40.0000000-04:00 (Local)
LastAccessTimeUtc = (DateTime) 2009-07-14T09:26:40.0000000Z (Utc)
LastWriteTime = (DateTime) 2009-07-14T05:26:40.0000000-04:00 (Local)
LastWriteTimeUtc = (DateTime) 2009-07-14T09:26:40.0000000Z (Utc)
Attributes = (FileAttributes) [ReadOnly, Directory] /* 0x00000011 */
}
VolumeLabel = (string) "GRMCPRFRER_EN_DVD"
}
}
历史
可以从 Google code 下载最新版本。