什么使Visual Studio调试器停止评估ToString重写?

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

最近,我一直在调试一些我的Noda Time代码,并且我注意到当我有一个NodaTime.Instant类型的局部变量(Noda Time中的一个中心struct类型)时,“本地语言”和“看”窗口似乎不会调用其ToString()覆盖。 如果我在监视窗口中显式调用ToString() ,我会看到适当的表示形式,但除此之外我只能看到:

variableName       {NodaTime.Instant}

这不是很有用。

如果我改变override来返回一个常量字符串,那么这个字符串会显示在调试器中,所以很明显它能够获取它的存在 - 它只是不想在“正常”状态下使用它。

我决定在一个小小的演示程序中在本地重现这一点,这就是我想出的。 (请注意,在这个岗位的早期版本, 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被禁用了。)

现在,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;

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


更新:

此错误已在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。在较高层次上,实现是:

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

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

  • 调试器不计算NodaTime.Instant.ToString - >这是因为它是struct类型,并且ToString的实现不能由调试器仿真,如上所述。
  • Thread.Sleep在结构体上由ToString调用时似乎需要零时间 - >这是因为模拟器正在执行ToString 。 Thread.Sleep是一种本地方法,但仿真器知道它并忽略该调用。 我们这样做是为了尝试获得向用户展示的价值。 在这种情况下延迟不会有帮助。
  • DisplayAttibute("ToString()")起作用。 - >这是令人困惑的。 隐式调用ToStringDebuggerDisplay之间唯一的区别在于隐式ToString评估的任何超时将禁用该类型的所有隐式ToString评估,直到下一个调试会话。 你可能正在观察那种行为。
  • 就设计问题/错误而言,这是我们计划在未来版本的Visual Studio中解决的问题。

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

    链接地址: http://www.djcxy.com/p/36067.html

    上一篇: What makes the Visual Studio debugger stop evaluating a ToString override?

    下一篇: Linking a precompiled MSIL file into an assembly?