首页 文章

是什么让Visual Studio调试器停止评估ToString覆盖?

提问于
浏览
219

环境:Visual Studio 2015 RTM . (我没有尝试旧版本 . )

最近,我一直在调试我的一些Noda Time代码,并且've noticed that when I'已经有一个 NodaTime.Instant 类型的局部变量(Noda Time中的一个中心 struct 类型),"Locals"和"Watch"窗口似乎没有调用它的 ToString() 覆盖 . 如果我在监视窗口中显式调用 ToString() ,我会看到相应的表示,但我只是看到:

variableName       {NodaTime.Instant}

这不是很有用 .

如果我更改覆盖以返回一个常量字符串,那么该字符串将显示在调试器中,因此's clearly able to pick up that it'在那里 - 它只是不想在其"normal"状态下使用它 .

我决定在一个小的演示应用程序中本地重现这个,并在这里提出's what I' . (请注意,在这篇文章的早期版本中, DemoStruct 是一个类, DemoClass 根本不存在 - 我的错,但它解释了一些看起来很奇怪的评论......)

using System;
using System.Diagnostics;
using System.Threading;

public struct DemoStruct
{
    public string Name { get; }

    public DemoStruct(string name)
    {
        Name = name;
    }

    public override string ToString()
    {
        Thread.Sleep(1000); // Vary this to see different results
        return $"Struct: {Name}";
    }
}

public class DemoClass
{
    public string Name { get; }

    public DemoClass(string name)
    {
        Name = name;
    }

    public override string ToString()
    {
        Thread.Sleep(1000); // Vary this to see different results
        return $"Class: {Name}";
    }
}

public class Program
{
    static void Main()
    {
        var demoClass = new DemoClass("Foo");
        var demoStruct = new DemoStruct("Bar");
        Debugger.Break();
    }
}

在调试器中,我现在看到:

demoClass    {DemoClass}
demoStruct   {Struct: Bar}

但是,如果我将 Thread.Sleep 调用从1秒减少到900毫秒,仍然会暂停一段时间,但之后我会看到 Class: Foo 为值 . DemoStruct.ToString() 中的 Thread.Sleep 调用持续多长时间似乎并不重要,它's always displayed properly - and the debugger displays the value before the sleep would have completed. (It' s就像 Thread.Sleep 被禁用一样 . )

现在Noda Time中的 Instant.ToString() 做了相当多的工作,但它肯定不需要一秒钟 - 所以可能有更多的条件导致调试器放弃评估 ToString() 调用 . 当然,无论如何它都是一个结构 .

我已经尝试过递归以查看它是否是堆栈限制,但似乎并非如此 .

那么,我怎样才能找出阻止VS完全评估 Instant.ToString() 的内容?如下所述, DebuggerDisplayAttribute 似乎有所帮助,但不知道为什么,我'm never going to be entirely confident in when I need it and when I don't .

Update

如果我使用DebuggerDisplayAttribute,事情会发生变化:

// For the sample code in the question...
[DebuggerDisplay("{ToString()}")]
public class DemoClass

给我:

demoClass      Evaluation timed out

而当我在野田时间申请时:

[DebuggerDisplay("{ToString()}")]
public struct Instant

一个简单的测试应用程序显示正确的结果:

instant    "1970-01-01T00:00:00Z"

因此,大概是Noda Time中的问题是 DebuggerDisplayAttribute 确实强制通过的一些条件 - 即使它没有强制通过超时 . (这符合我的预期,即 Instant.ToString 足够快,足以避免超时 . )

这可能是一个足够好的解决方案 - 但我正在继续,以及我是否可以简单地更改代码以避免必须将属性放在Noda Time中的所有各种值类型上 .

Curiouser and curiouser

无论什么令人困惑,调试器有时只会混淆它 . 让我们创建一个包含 Instant 的类,并将其用于自己的 ToString() 方法:

using NodaTime;
using System.Diagnostics;

public class InstantWrapper
{
    private readonly Instant instant;

    public InstantWrapper(Instant instant)
    {
        this.instant = instant;
    }

    public override string ToString() => instant.ToString();
}

public class Program
{
    static void Main()
    {
        var instant = NodaConstants.UnixEpoch;
        var wrapper = new InstantWrapper(instant);

        Debugger.Break();
    }
}

现在我最终看到了:

instant    {NodaTime.Instant}
wrapper    {1970-01-01T00:00:00Z}

但是,根据Eren在评论中的建议,如果我将 InstantWrapper 更改为结构,我得到:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

所以它可以评估 Instant.ToString() - 只要这是由另一个 ToString 方法调用的......这是在一个类中 . 基于所显示变量的类型,类/结构部分似乎很重要,而不是为了获得结果而需要执行的代码 .

作为另一个例子,如果我们使用:

object boxed = NodaConstants.UnixEpoch;

...然后它工作正常,显示正确的 Value . 让我困惑的颜色 .

1 回答

  • 191

    更新:

    Visual Studio 2015 Update 2中已修复此错误 . 如果您仍然遇到使用Update 2或更高版本在结构值上评估ToString的问题,请告诉我 .

    原文答案:

    您正在使用Visual Studio 2015遇到已知的错误/设计限制,并在结构类型上调用ToString . 处理 System.DateTimeSpan 时也可以观察到这一点 . System.DateTimeSpan.ToString() 在Visual Studio 2013的评估窗口中工作,但在2015年并不总是有效 .

    如果您对低级详细信息感兴趣,请按以下步骤操作:

    为了评估 ToString ,调试器执行所谓的"function evaluation" . 简单来说,调试器暂停除当前线程之外的进程中的所有线程,将当前线程的上下文更改为 ToString 函数,设置隐藏的保护断点,然后允许进程继续 . 当命中保护断点时,调试器将进程恢复到先前的状态,并使用函数的返回值来填充窗口 .

    为了支持lambda表达式,我们必须在Visual Studio 2015中完全重写CLR Expression Evaluator . 在高级别,实现是:

    • Roslyn为表达式/局部变量生成MSIL代码,以获取要以各种方式显示的值检查窗口 .

    • 调试器解释IL以获得结果 .

    • 如果有任何"call"指令,则调试器执行如上所述的功能评估 .

    • 调试器/ roslyn获取此结果并将其格式化为向用户显示的树状视图 .

    由于IL的执行,调试器总是处理"real"和"fake"值的复杂混合 . 实际值实际存在于正在调试的进程中 . 假值仅存在于调试器进程中 . 要实现正确的结构语义,调试器总是需要在将结构值推送到IL堆栈时复制该值 . 复制的值不再是"real"值,现在仅存在于调试器进程中 . 这意味着如果我们以后需要执行 ToString 的功能评估,我们就可以在这个过程中存在't because the value doesn't . 要尝试获取值,我们需要模拟 ToString 方法的执行 . 虽然我们可以模仿一些东西,但有许多限制 . 例如,我们可以't emulate native code and we can' t执行对"real"委托值的调用或对反射值的调用 .

    考虑到所有这些,这就是造成您所看到的各种行为的原因:

    • 调试器未评估 NodaTime.Instant.ToString - >这是因为它是结构类型,并且调试器无法模拟ToString的实现,如上所述 .

    • Thread.Sleep 在结构上由 ToString 调用时似乎占用零时间 - >这是因为模拟器正在执行 ToString . Thread.Sleep是一种本机方法,但模拟器知道它并且只是忽略了调用 . 我们这样做是为了尝试获取向用户显示的值 . 在这种情况下,延迟没有帮助 .

    • DisplayAttibute("ToString()") 有效 . - >那令人困惑 . 隐式调用 ToStringDebuggerDisplay 之间的唯一区别是,隐式 ToString 评估的任何超时都将禁用该类型的所有隐式 ToString 评估,直到下一个调试会话 . 你可能正在观察这种行为 .

    就设计问题/错误而言,这是我们计划在将来的Visual Studio版本中解决的问题 .

    希望能够解决问题 . 如果您有更多问题,请与我们联系 . :-)

相关问题