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

Jon*_*eet 220 c# debugging visual-studio-2015

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

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

variableName       {NodaTime.Instant}
Run Code Online (Sandbox Code Playgroud)

这不是很有用.

如果我更改覆盖以返回一个常量字符串,那么该字符串显示在调试器中,因此它显然能够接收它 - 它只是不想在"正常"状态下使用它.

我决定在一个小的演示应用程序中本地重现这个,这就是我想出的.(请注意,在这篇文章的早期版本中,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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

demoClass    {DemoClass}
demoStruct   {Struct: Bar}
Run Code Online (Sandbox Code Playgroud)

但是,如果我将Thread.Sleep呼叫从1秒减少到900毫秒,仍然会有短暂停顿,但后来我将其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
Run Code Online (Sandbox Code Playgroud)

给我:

demoClass      Evaluation timed out
Run Code Online (Sandbox Code Playgroud)

而当我在野田时间申请时:

[DebuggerDisplay("{ToString()}")]
public struct Instant
Run Code Online (Sandbox Code Playgroud)

一个简单的测试应用程序显示正确的结果:

instant    "1970-01-01T00:00:00Z"
Run Code Online (Sandbox Code Playgroud)

所以想必在Noda时间的问题是一些条件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();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我最终看到了:

instant    {NodaTime.Instant}
wrapper    {1970-01-01T00:00:00Z}
Run Code Online (Sandbox Code Playgroud)

但是,根据Eren在评论中提出的建议,如果我改为InstantWrapper结构,我得到:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}
Run Code Online (Sandbox Code Playgroud)

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

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

object boxed = NodaConstants.UnixEpoch;
Run Code Online (Sandbox Code Playgroud)

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

Pat*_*SFT 193

更新:

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. 如果存在任何"调用"指令,则调试器执行如上所述的功能评估.
  4. 调试器/ roslyn获取此结果并将其格式化为向用户显示的树状视图.

由于IL的执行,调试器总是处理"真实"和"假"值的复杂混合.实际值实际存在于正在调试的进程中.假值仅存在于调试器进程中.要实现正确的结构语义,调试器总是需要在将结构值推送到IL堆栈时复制该值.复制的值不再是"实际"值,现在只存在于调试器进程中.这意味着如果我们以后需要执行功能评估ToString,我们就不能因为该过程中不存在该值.要尝试获取我们需要模拟执行的值ToString方法.虽然我们可以模仿一些东西,但有许多限制.例如,我们无法模拟本机代码,也无法执行对"真实"委托值的调用或对反射值的调用.

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

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

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

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

  • 理想情况下,我们希望CLR执行所有操作.这提供了最准确和可靠的结果.这就是我们为ToString调用进行实际功能评估的原因.如果无法做到这一点,我们会回过头来模仿这个电话.这意味着调试器假装是执行该方法的CLR.显然,如果实现是<code>返回"Hello"</ code>,这很容易做到.如果实现执行P-Invoke,则更难或不可能. (8认同)
  • @tzachs,模拟器完全是单线程的.如果`innerResult`从null开始,循环将永远不会终止,最终评估将超时.实际上,评估只允许进程中的单个线程默认运行,因此无论是否使用模拟器,您都将看到相同的行为. (3认同)
  • 顺便说一句,如果您知道您的评估需要多个线程,请查看[Debugger.NotifyOfCrossThreadDependency](https://msdn.microsoft.com/en-us/library/system.diagnostics.debugger.notifyofcrossthreaddependency(v = vs.110) )的.aspx).调用此方法将中止评估,并显示一条消息,指出评估需要运行所有线程,调试器将提供用户可以推送以强制进行评估的按钮.缺点是在评估期间在其他线程上遇到的任何断点都将被忽略. (2认同)