关于c#:是什么让Visual Studio调试器停止评估ToString覆盖?

What makes the Visual Studio debugger stop evaluating a ToString override?

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

最近,我调试了一些noda时间代码,我注意到当我有一个NodaTime.Instant类型的局部变量(noda time中的中心struct类型之一)时,"局部变量"和"监视"窗口似乎不会调用它的ToString()覆盖。如果我在监视窗口中明确地调用ToString(),我会看到适当的表示,但否则我只会看到:

1
variableName       {NodaTime.Instant}

这不是很有用。

如果我将重写更改为返回一个常量字符串,该字符串将显示在调试器中,因此它很明显能够识别出它在那里——它只是不想在其"正常"状态下使用它。

我决定在本地的一个小演示应用程序中复制这个,这是我想到的。(注意,在这篇文章的早期版本中,DemoStruct是一个类,DemoClass根本不存在——我的错,但它解释了一些现在看起来很奇怪的评论…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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();
    }
}

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

1
2
demoClass    {DemoClass}
demoStruct   {Struct: Bar}

但是,如果我将Thread.Sleep调用从1秒减少到900ms,仍然会有一个短暂的停顿,但是我会看到Class: Foo作为值。无论Thread.Sleep调用在DemoStruct.ToString()中的时间有多长,它总是正确显示-并且调试器在睡眠完成之前显示值。(就好像Thread.Sleep被禁用了一样。)

现在noda time中的Instant.ToString()做了相当多的工作,但肯定不会花一整秒钟,因此可能存在更多的条件导致调试器放弃评估ToString()调用。当然,它是一个结构。

我试过递归,看看它是否是堆栈限制,但情况似乎不是这样。

那么,我如何才能计算出阻止和全面评估Instant.ToString()的因素呢?如下文所述,DebuggerDisplayAttribute似乎有帮助,但不知道为什么,我永远不会完全有信心在什么时候需要它,什么时候不需要它。

更新

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

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

给我:

1
demoClass      Evaluation timed out

而当我在noda时间应用它时:

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

一个简单的测试应用程序向我展示了正确的结果:

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

所以可以推测,野田佳彦时期的问题是某个条件,即DebuggerDisplayAttribute确实强制通过——即使它不强制通过超时。(这符合我对Instant.ToString的期望,它的速度很快,可以避免超时。)

这可能是一个很好的解决方案——但我仍然想知道发生了什么,以及是否可以简单地更改代码,以避免在noda时间内将属性放在所有不同的值类型上。

保管人和保管人

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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();
    }
}

现在我看到:

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

然而,根据EREN在评论中的建议,如果我将InstantWrapper改为结构,我得到:

1
2
instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

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

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

1
object boxed = NodaConstants.UnixEpoch;

…然后它工作正常,显示正确的值。把我弄糊涂了。


更新:

此错误已在Visual Studio 2015更新2中修复。如果使用更新2或更高版本评估结构值的字符串时仍遇到问题,请通知我。

原始答案:

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

如果你对低层次的细节感兴趣,下面是发生的事情:

为了评估ToString,调试器执行所谓的"函数评估"。在大大简化的术语中,调试器挂起进程中除当前线程以外的所有线程,将当前线程的上下文更改为ToString函数,设置隐藏的保护断点,然后允许进程继续。当命中保护断点时,调试器会将进程恢复到以前的状态,并使用函数的返回值填充窗口。

为了支持lambda表达式,我们必须在Visual Studio 2015中完全重写clr表达式计算器。在高层次上,实施是:

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

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

  • 调试器没有评估NodaTime.Instant.ToString->这是因为它是结构类型,ToString的实现不能如上文所述,由调试器模拟。
  • ToString调用struct->这是因为仿真器正在执行ToString。thread.sleep是一个本机方法,但模拟器知道而忽略了这个电话。我们这样做是为了获得价值向用户显示。在这种情况下,延迟是没有帮助的。
  • DisplayAttibute("ToString()")工程。->那太令人困惑了。唯一ToString的隐式调用与DebuggerDisplay是隐式ToString的任何超时。评估将禁用所有隐含的ToString评估。键入直到下一个调试会话。你可能在观察行为。
  • 在设计问题/缺陷方面,我们计划在未来的Visual Studio版本中解决这一问题。

    希望能把事情弄清楚。如果有更多问题,请告诉我。-)