环境:Visual Studio 2015 RTM。(我还没有尝试过旧版本。)
最近,我一直在调试我的一些NodaTime代码,我注意到当我有一个类型为NodaTime的局部变量时。即时(野田时间的中心结构类型之一),“本地”和“观看”窗口似乎不调用它的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}
然而,如果我减少线程。睡眠调用从1秒下降到900ms,仍然有一个短暂的暂停,但随后我看到Class: Foo作为值。这条线有多长似乎并不重要。睡眠调用在DemoStruct.ToString()中,它总是正确地显示——调试器在睡眠完成之前显示该值。(就像Thread。睡眠被禁用。)
现在,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强制通过-即使它不强制通过超时。(这将符合我的期望,即时。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更改为struct,我得到:
instant {NodaTime.Instant}
wrapper {InstantWrapper}
所以它可以计算Instant.ToString() -只要由另一个ToString方法调用…这是在一个类中。类/结构部分似乎很重要,这取决于所显示变量的类型,而不是代码需要什么
为了得到结果而执行。
另一个例子是,如果我们用:
object boxed = NodaConstants.UnixEpoch;
... 然后它正常工作,显示正确的值。把我弄糊涂了。
更新:
此错误已在Visual Studio 2015更新2中修复。如果您仍然在使用Update 2或更新版本对结构值计算ToString时遇到问题,请告诉我。
最初的回答:
你在Visual Studio 2015中遇到了一个已知的错误/设计限制,并在结构类型上调用ToString。在处理System.DateTimeSpan时也可以观察到这一点。System.DateTimeSpan.ToString()在Visual Studio 2013的评估窗口中工作,但在2015中并不总是工作。
如果你对底层细节感兴趣,下面是正在发生的事情:
为了求值ToString,调试器会执行所谓的“函数求值”。简单来说,调试器挂起进程中除当前线程外的所有线程,将当前线程的上下文更改为ToString函数,设置隐藏的保护断点,然后允许进程继续。当遇到保护断点时,调试器将进程恢复到以前的状态,并使用函数的返回值填充窗口。
为了支持lambda表达式,我们不得不在Visual Studio 2015中完全重写CLR表达式求值器。在高层次上,实现是:
Roslyn为表达式/局部变量生成MSIL代码,以获得要在各种检查窗口中显示的值。
调试器解释IL以获得结果。
如果有任何“调用”指令,调试器将执行一个
如上所述的函数求值。
调试器/roslyn获取此结果并将其格式化到
显示给用户的树形视图。
Because of the execution of IL, the debugger is always dealing with a complicated mix of "real" and "fake" values. Real values actually exist in the process being debugged. Fake values only exist in the debugger process. To implement proper struct semantics, the debugger always needs to make a copy of the value when pushing a struct value to the IL stack. The copied value is no longer a "real" value and now only exists in the debugger process. That means if we later need to perform function evaluation of ToString, we can't because the value doesn't exist in the process. To try and get the value we need to emulate execution of the ToString method. While we can emulate some things, there are many limitations. For example, we can't emulate native code and we can't execute calls to "real" delegate values or calls on reflection values.
考虑到所有这些,以下是导致你看到的各种行为的原因:
The debugger isn't evaluating NodaTime.Instant.ToString -> This is
because it is struct type and the implementation of ToString can't
be emulated by the debugger as described above.
Thread.Sleep seems to take zero time when called by ToString on a
struct -> This is because the emulator is executing ToString.
Thread.Sleep is a native method, but the emulator is aware
of it and just ignores the call. We do this to try and get a value
to show to the user. A delay wouldn't be helpful in this case.
DisplayAttibute("ToString()") works. -> That is confusing. The only
difference between the implicit calling of ToString and
DebuggerDisplay is that any time-outs of the implicit ToString
evaluation will disable all implicit ToString evaluations for that
type until the next debug session. You may be observing that
behavior.
在设计问题/bug方面,这是我们计划在Visual Studio的未来版本中解决的问题。
希望这些都讲清楚了。如果你还有其他问题,请告诉我。: -)
更新:
此错误已在Visual Studio 2015更新2中修复。如果您仍然在使用Update 2或更新版本对结构值计算ToString时遇到问题,请告诉我。
最初的回答:
你在Visual Studio 2015中遇到了一个已知的错误/设计限制,并在结构类型上调用ToString。在处理System.DateTimeSpan时也可以观察到这一点。System.DateTimeSpan.ToString()在Visual Studio 2013的评估窗口中工作,但在2015中并不总是工作。
如果你对底层细节感兴趣,下面是正在发生的事情:
为了求值ToString,调试器会执行所谓的“函数求值”。简单来说,调试器挂起进程中除当前线程外的所有线程,将当前线程的上下文更改为ToString函数,设置隐藏的保护断点,然后允许进程继续。当遇到保护断点时,调试器将进程恢复到以前的状态,并使用函数的返回值填充窗口。
为了支持lambda表达式,我们不得不在Visual Studio 2015中完全重写CLR表达式求值器。在高层次上,实现是:
Roslyn为表达式/局部变量生成MSIL代码,以获得要在各种检查窗口中显示的值。
调试器解释IL以获得结果。
如果有任何“调用”指令,调试器将执行一个
如上所述的函数求值。
调试器/roslyn获取此结果并将其格式化到
显示给用户的树形视图。
Because of the execution of IL, the debugger is always dealing with a complicated mix of "real" and "fake" values. Real values actually exist in the process being debugged. Fake values only exist in the debugger process. To implement proper struct semantics, the debugger always needs to make a copy of the value when pushing a struct value to the IL stack. The copied value is no longer a "real" value and now only exists in the debugger process. That means if we later need to perform function evaluation of ToString, we can't because the value doesn't exist in the process. To try and get the value we need to emulate execution of the ToString method. While we can emulate some things, there are many limitations. For example, we can't emulate native code and we can't execute calls to "real" delegate values or calls on reflection values.
考虑到所有这些,以下是导致你看到的各种行为的原因:
The debugger isn't evaluating NodaTime.Instant.ToString -> This is
because it is struct type and the implementation of ToString can't
be emulated by the debugger as described above.
Thread.Sleep seems to take zero time when called by ToString on a
struct -> This is because the emulator is executing ToString.
Thread.Sleep is a native method, but the emulator is aware
of it and just ignores the call. We do this to try and get a value
to show to the user. A delay wouldn't be helpful in this case.
DisplayAttibute("ToString()") works. -> That is confusing. The only
difference between the implicit calling of ToString and
DebuggerDisplay is that any time-outs of the implicit ToString
evaluation will disable all implicit ToString evaluations for that
type until the next debug session. You may be observing that
behavior.
在设计问题/bug方面,这是我们计划在Visual Studio的未来版本中解决的问题。
希望这些都讲清楚了。如果你还有其他问题,请告诉我。: -)