【问题标题】:What makes the Visual Studio debugger stop evaluating a ToString override?是什么让 Visual Studio 调试器停止评估 ToString 覆盖?
【发布时间】:2015-10-12 07:24:02
【问题描述】:

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

最近,我一直在调试我的一些 Noda Time 代码,并且我注意到当我有一个 NodaTime.Instant 类型的局部变量时(野田时间的中心 struct 类型之一) ,“Locals”和“Watch”窗口似乎没有调用其ToString() 覆盖。如果我在监视窗口中显式调用ToString(),我会看到适当的表示,否则我只会看到:

variableName       {NodaTime.Instant}

这不是很有用。

如果我将覆盖更改为返回一个常量字符串,则该字符串 会显示在调试器中,因此它可以清楚地识别出它的存在 - 它只是不想在它的“正常”状态。

我决定在一个小演示应用程序中在本地重现这一点,这就是我想出的。 (请注意,在这篇文章的早期版本中,DemoStruct 是一个类,而 DemoClass 根本不存在 - 我的错,但它解释了一些现在看起来很奇怪的 cmets......)

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 很容易足够快以避免超时。)

可能是一个足够好的解决方案 - 但我仍然想知道发生了什么,以及我是否可以更改代码以避免必须将属性放在所有不同的值上输入野田时间。

好奇者和好奇者

无论什么让调试器感到困惑,有时只会让它感到困惑。让我们创建一个持有 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}

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

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

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

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

object boxed = NodaConstants.UnixEpoch;

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

【问题讨论】:

  • @John 在 VS 2013 中的相同行为(我不得不删除 c#6 的东西),并附加一条消息:名称函数评估已禁用,因为之前的函数评估超时。您必须继续执行才能重新启用函数评估。字符串
  • 欢迎使用 c# 6.0 @3-14159265358979323846264
  • 也许DebuggerDisplayAttribute 会让它更加努力。
  • 您可以在注册表中更改超时...stackoverflow.com/a/1212068/5040941。 (感谢@Neel)!
  • @DiomidisSpinellis:好吧,我已经在这里问过了,以便 a) 以前见过相同事物或了解 VS 内部的人可以回答; b) 以后遇到同样问题的人都可以很快得到答案。

标签: c# debugging visual-studio-2015


【解决方案1】:

更新:

此错误已在 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 表达式求值器。在高层次上,实现是:

  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 中解决的问题。

希望这可以解决问题。如果您有更多问题,请告诉我。 :-)

【讨论】:

  • 如果实现只是“返回字符串文字”,您知道 Instant.ToString 是如何工作的吗?听起来还有一些复杂性仍未解决:) 我会检查我是否真的可以重现这种行为......
  • @Jon,我不确定你在问什么。调试器在进行真正的函数评估时与实现无关,它总是首先尝试这个。调试器只在需要模拟调用时才关心实现 - 返回字符串文字是最简单的模拟情况。
  • 理想情况下,我们希望 CLR 执行所有操作。这提供了最准确和可靠的结果。这就是为什么我们对 ToString 调用进行真正的函数评估的原因。当这不可能时,我们会退回到模拟调用。这意味着调试器假装是执行该方法的 CLR。显然,如果实现是return "Hello",这很容易做到。如果实现执行 P-Invoke,则更加困难或不可能。
  • @tzachs,模拟器完全是单线程的。如果innerResult 以null 开始,则循环将永远不会终止,最终评估将超时。事实上,默认情况下,评估只允许进程中的单个线程运行,因此无论是否使用模拟器,您都会看到相同的行为。
  • 顺便说一句,如果您知道您的评估需要多个线程,请查看Debugger.NotifyOfCrossThreadDependency。调用此方法将中止评估,并显示一条消息,说明评估需要所有线程都运行,并且调试器将提供一个用户可以按下以强制评估的按钮。缺点是在评估期间命中其他线程的任何断点都将被忽略。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-09-19
  • 1970-01-01
  • 2014-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多