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


221

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

最近,我一直在调试一些Noda Time代码,并且注意到当我获得类型的局部变量NodaTime.InstantstructNoda 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秒减少到900ms,仍然会有短暂的停顿,但是我认为Class: Foo这是有价值的。Thread.Sleep调用的持续时间似乎并不重要DemoStruct.ToString(),它始终可以正确显示-调试器会在睡眠完成之前显示该值。(好像Thread.Sleep已被禁用。)

现在,Instant.ToString()在Noda Time中可以进行大量工作,但是肯定不需要花费一秒钟的时间-因此,可能还有更多条件导致调试器放弃评估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中将属性放在所有各种值类型上,所以我仍然想知道。

越来越好奇

令人困惑的是,调试器有时只会使其混乱。让我们创建一个包含的类,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;

...然后工作正常,显示正确的值。使我困惑。


7
@John在VS 2013中具有相同的行为(我必须删除c#6内容),并附加一条消息:名称函数评估被禁用,因为先前的函数评估超时。您必须继续执行才能重新启用功能评估。字符串
vc 2015年

1
欢迎使用c#6.0 @ 3-14159265358979323846264
Neel

1
也许DebuggerDisplayAttribute会导致它尝试更努力。
罗林2015年

1
看到它是第5点neelbhatt40.wordpress.com/2015/07/13/…@ 3-14159265358979323846264对于新的c#6.0
Neel

5
@DiomidisSpinellis:好吧,我在这里问过这个问题是为了:a)以前见过相同事物或知道VS内部的人可以回答;b)将来遇到相同问题的任何人都可以快速获得答案。
乔恩·斯基特

Answers:


193

更新:

此错误已在Visual Studio 2015 Update 2中修复。让我知道您是否仍然遇到使用Update 2或更高版本评估结构值上的ToString的问题。

原始答案:

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

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

为了评估ToString,调试器执行所谓的“功能评估”。用大大简化的术语来说,调试器将挂起进程中除当前线程之外的所有线程,将当前线程的上下文更改为ToString函数,设置隐藏的保护断点,然后允许进程继续。当击中保护断点时,调试器将进程还原到其先前的状态,并且该函数的返回值用于填充窗口。

为了支持lambda表达式,我们必须在Visual Studio 2015中完全重写CLR Expression Evaluator。从总体上讲,实现是:

  1. Roslyn为表达式/局部变量生成MSIL代码,以获取要在各个检查窗口中显示的值。
  2. 调试器解释IL以获取结果。
  3. 如果有任何“调用”指令,则调试器将如上所述执行功能评估。
  4. 调试器/ roslyn获取此结果并将其格式化为显示给用户的树状视图。

由于执行IL,调试器始终处理“真实”和“伪”值的复杂混合。实际值实际存在于要调试的进程中。伪值仅存在于调试器过程中。为了实现适当的结构语义,当将结构值推入IL堆栈时,调试器始终需要复制该值。复制的值不再是“真实”值,现在仅存在于调试器进程中。这意味着,如果我们以后需要对进行功能评估ToString,就不能这样做,因为该值在流程中不存在。要尝试获取该值,我们需要模拟执行ToString方法。虽然我们可以模仿一些东西,但有很多限制。例如,我们不能模拟本地代码,也不能执行对“真实”委托值的调用或对反射值的调用。

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

  1. 调试器未评估NodaTime.Instant.ToString->这是因为它是struct类型,并且调试器无法如上所述仿真ToString的实现。
  2. Thread.SleepToString在struct上调用时,似乎需要零时间->这是因为仿真器正在执行ToString。Thread.Sleep是本机方法,但是仿真器知道它,只是忽略了调用。我们这样做是为了尝试向用户显示一个值。在这种情况下,延迟不会有帮助。
  3. DisplayAttibute("ToString()")作品。->令人困惑。ToString和 的隐式调用之间的唯一区别DebuggerDisplay是,任何隐式ToString 评估超时都将禁用ToString该类型的所有隐式评估,直到下一次调试会话为止。您可能正在观察该行为。

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

希望这可以清除一切。如果您还有其他问题,请告诉我。:-)


1
如果实现只是“返回字符串文字”,是否知道Instant.ToString如何工作?听起来似乎还有一些复杂的原因尚未解决:)我将检查我是否真的可以重现这种行为……
乔恩·斯凯特

1
@乔恩,我不确定你在问什么。在进行实函数评估时,调试器与实现无关,并且始终会首先尝试进行调试。调试器仅在需要模拟调用时才关心实现-返回字符串文字是最简单的模拟情况。
Patrick Nelson-MSFT 2015年

8
理想情况下,我们希望CLR执行所有操作。这提供了最准确和可靠的结果。这就是为什么我们对ToString调用进行实函数评估。如果无法做到这一点,我们将退回去模拟呼叫。这意味着调试器假装为执行该方法的CLR。显然,如果实现是<code>返回“ Hello” </ code>,则很容易做到。如果实现进行P调用,则将变得更加困难或不可能。
Patrick Nelson-MSFT 2015年

3
@tzachs,模拟器完全是单线程的。如果innerResult从null开始,则循环将永远不会终止,并且最终评估将超时。实际上,评估默认情况下仅允许进程中的单个线程运行,因此无论是否使用模拟器,您都将看到相同的行为。
Patrick Nelson-MSFT 2015年

2
顺便说一句,如果您知道评估需要多个线程,请查看Debugger.NotifyOfCrossThreadDependency。调用此方法将中止评估,并显示一条消息,指出评估需要运行所有线程,并且调试器将提供一个按钮,用户可以按此按钮来强制评估。缺点是在评估过程中碰到其他线程的任何断点将被忽略。
Patrick Nelson-MSFT 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.