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

时间:2015-07-22 12:12:22

标签: c# debugging visual-studio-2015

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

最近,我一直在调试我的一些Noda Time代码,我注意到当我有一个NodaTime.Instant类型的局部变量时(其中一个struct在Noda Time中的类型,“Locals”和“Watch”窗口似乎没有调用其ToString()覆盖。如果我在观察窗口中明确地调用ToString(),我会看到相应的表示,但我只是看到:

variableName       {NodaTime.Instant}

这不是很有用。

如果我更改覆盖以返回一个常量字符串,则字符串 显示在调试器中,因此它显然能够接收它 - 它只是不想在它中使用它它的“正常”状态。

我决定在一个小型演示应用程序中本地重现这个,这就是我想出来的。 (请注意,在这篇文章的早期版本中,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作为值。 Thread.Sleep调用在DemoStruct.ToString()中的持续时间似乎并不重要,它总是正确显示 - 并且调试器会在睡眠完成之前显示该值。 (好像Thread.Sleep被禁用了。)

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

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

那么,我怎样才能找出阻止VS完全评估Instant.ToString()的内容?如下所述,DebuggerDisplayAttribute似乎有所帮助,但在不知道为什么的情况下,我永远不会完全相信我何时需要它以及什么时候不需要。

更新

如果我使用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和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;

...然后它工作正常,显示正确的值。让我感到困惑。

1 个答案:

答案 0 :(得分:192)

更新

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

原始答案:

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

如果您对低级详细信息感兴趣,请参阅以下内容:

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

为了支持lambda表达式,我们必须在Visual Studio 2015中完全重写CLR Expression Evaluator。在较高的层次上,实现是:

  1. Roslyn为表达式/局部变量生成MSIL代码,以获取要在各种检查窗口中显示的值。
  2. 调试器解释IL以获得结果。
  3. 如果有任何“调用”指令,则调试器执行a 功能评估如上所述。
  4. 调试器/ roslyn获取此结果并将其格式化为 向用户显示的树状视图。
  5. 由于IL的执行,调试器总是处理“真实”和“假”值的复杂混合。实际值实际存在于正在调试的进程中。假值仅存在于调试器进程中。要实现正确的结构语义,调试器总是需要在将结构值推送到IL堆栈时复制该值。复制的值不再是“实际”值,现在只存在于调试器进程中。这意味着如果我们以后需要执行ToString的功能评估,我们不能因为该过程中不存在该值。要尝试获取值,我们需要模拟ToString方法的执行。虽然我们可以模仿一些东西,但有许多限制。例如,我们无法模拟本机代码,也无法执行对“真实”委托值的调用或对反射值的调用。

    考虑到所有这一切,以下是导致您所看到的各种行为的原因:

    1. 调试器未评估NodaTime.Instant.ToString - >这是 因为它是结构类型而且ToString的实现不能 如上所述由调试器模拟。
    2. {li> Thread.Sleep在被ToString调用时似乎没有时间 struct - >这是因为模拟器正在执行ToString。 Thread.Sleep是一种本机方法,但模拟器知道 它只是忽略了电话。我们这样做是为了尝试获得价值 向用户显示。在这种情况下,延迟没有帮助。
    3. DisplayAttibute("ToString()")有效。 - >这令人困惑。唯一的 隐式调用ToString和。{ DebuggerDisplay是隐含ToString的任何超时 评估将禁用所有隐式ToString评估 键入,直到下一个调试会话。你可能正在观察它 行为。
    4. 就设计问题/错误而言,这是我们计划在Visual Studio的未来版本中解决的问题。

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