环境:Visual Studio 2015 RTM。(我没有试过旧版本。)
最近,我调试了一些noda时间代码,我注意到当我有一个NodaTime.Instant类型的局部变量(noda time中的中心struct类型之一)时,"局部变量"和"监视"窗口似乎不会调用它的ToString()覆盖。如果我在监视窗口中明确地调用ToString(),我会看到适当的表示,但否则我只会看到:
1
| variableName {NodaTime.Instant} |
这不是很有用。
如果我将重写更改为返回一个常量字符串,该字符串将显示在调试器中,因此它很明显能够识别出它在那里——它只是不想在其"正常"状态下使用它。
我决定在本地的一个小演示应用程序中复制这个,这是我想到的。(注意,在这篇文章的早期版本中,DemoStruct是一个类,DemoClass根本不存在——我的错,但它解释了一些现在看起来很奇怪的评论…)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| 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();
}
} |
在调试器中,我现在看到:
1 2
| demoClass {DemoClass}
demoStruct {Struct: Bar} |
但是,如果我将Thread.Sleep调用从1秒减少到900ms,仍然会有一个短暂的停顿,但是我会看到Class: Foo作为值。无论Thread.Sleep调用在DemoStruct.ToString()中的时间有多长,它总是正确显示-并且调试器在睡眠完成之前显示值。(就好像Thread.Sleep被禁用了一样。)
现在noda time中的Instant.ToString()做了相当多的工作,但肯定不会花一整秒钟,因此可能存在更多的条件导致调试器放弃评估ToString()调用。当然,它是一个结构。
我试过递归,看看它是否是堆栈限制,但情况似乎不是这样。
那么,我如何才能计算出阻止和全面评估Instant.ToString()的因素呢?如下文所述,DebuggerDisplayAttribute似乎有帮助,但不知道为什么,我永远不会完全有信心在什么时候需要它,什么时候不需要它。
更新
如果我使用DebuggerDisplayAttribute,情况会发生变化:
1 2 3
| // For the sample code in the question...
[DebuggerDisplay("{ToString()}")]
public class DemoClass |
给我:
1
| demoClass Evaluation timed out |
而当我在noda时间应用它时:
1 2
| [DebuggerDisplay("{ToString()}")]
public struct Instant |
一个简单的测试应用程序向我展示了正确的结果:
1
| instant "1970-01-01T00:00:00Z" |
所以可以推测,野田佳彦时期的问题是某个条件,即DebuggerDisplayAttribute确实强制通过——即使它不强制通过超时。(这符合我对Instant.ToString的期望,它的速度很快,可以避免超时。)
这可能是一个很好的解决方案——但我仍然想知道发生了什么,以及是否可以简单地更改代码,以避免在noda时间内将属性放在所有不同的值类型上。
保管人和保管人
无论是什么令人困惑的东西,调试程序只是有时会让它困惑。让我们创建一个类,它包含一个Instant并将其用于自己的ToString()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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();
}
} |
现在我看到:
1 2
| instant {NodaTime.Instant}
wrapper {1970-01-01T00:00:00Z} |
然而,根据EREN在评论中的建议,如果我将InstantWrapper改为结构,我得到:
1 2
| instant {NodaTime.Instant}
wrapper {InstantWrapper} |
所以它可以对Instant.ToString()进行评估,只要它是由另一个ToString方法调用的…在一个班里。类/结构部分似乎很重要,这取决于所显示变量的类型,而不是代码所需的类型。为了得到结果而执行。
作为另一个例子,如果我们使用:
1
| object boxed = NodaConstants.UnixEpoch; |
…然后它工作正常,显示正确的值。把我弄糊涂了。
- $"Struct: {Name}"…$是怎么回事?这是一个新的语言特性还是我以前从未见过的另一个特性?!
- @约翰在2013年的Vs中也有同样的行为(我不得不删除了C 6的东西),另外还有一条消息:name函数evaluation被禁用,因为之前的函数evaluation超时了。必须继续执行才能重新启用函数evaluation.string
- 欢迎来到C 6.0@3-14159265358979323846264
- 也许一个DebuggerDisplayAttribute会让它更努力一点。
- @罗林:但是我要为如何格式化它指定什么呢?覆盖ToString的要点是它已经提供了正确的格式。我不想选择一个特定的属性…
- @Jonskeet只是想知道,将属性指向ToString是否会诱使它等待更长的时间。(或者避免新的"回到违约的ToString"行为,以不同的方式失败?)
- @罗林:用我的示例代码尝试[DebuggerDisplay("{ToString()}")]将其更改为"评估超时":0
- 嗯,那可能有点帮助:p
- 看第5点neelbhatt40.wordpress.com/2015/07/13/…@3-1415926535897932384664新的C 6.0
- 与VS2010专业版相同的行为
- @劳林:哦-在野田佳彦的时候尝试一下确实有效,因为某些原因。当我有机会的时候会用它更新帖子-谢谢!我还是想知道为什么需要它-看起来很奇怪…
- 您可以在注册表中更改超时…stackoverflow.com/a/1212068/5040941。(谢谢@neel)!
- 我创建了"normalEvalTimeout"并将其设置为2000(hex,变成8192),它仍然超时,如问题中所述。注册表中不存在超时值,因此它可能不适用于2015年。
- 我还注意到,如果我为结构创建一个超时参数,并首先用900构造一个超时参数,然后用1000构造一个超时参数(在断点之前和之后),用1000表示类型,而用900表示ToString值。但是,如果我切换它们,它们都会显示类型。换言之,当本地窗口/疏散引擎认为速度太慢时,它似乎会记住先前的ToString和Bumps问题。
- @卡尔森:对。这不会让我吃惊-不过很高兴知道。
- 我还注意到了其他一些事情。我创建了三个变量,一个在断点之前,两个在断点之后。它们的超时时间分别为800、1000和800。当我跨过这一步时,它会显示struct:bar,null,null,然后struct:bar,demostruct,null,然后demostruct,demostruct。似乎当发现ToString花费了太多时间时,它会重新评估内容,然后默认返回到类型。
- 请注意,我看到了与您描述的问题完全相同的问题,以及我在以前的注释中测试过的问题,即使我切换到普通的旧字符串格式。字符串插值是一条红鲱鱼(或者可能我只是不理解问题底部的要点)
- 如果您的意思是DebuggerDisplay是否使用字符串插值,那么不是。这种语法已经被允许了很长一段时间,所以它应该可以在旧的.NET版本和Visual Studio版本上正常工作。我在2012年对此进行了测试,实际上它的工作原理与2015年完全相同,只是显示为字符串(就像我在评论中所描述的那样)。ToString使用您所显示的语法。
- @卡尔森:谢谢-把C 6周围的部分去掉,作为一个红鲱鱼。
- 使用我的3个结构和8001000800超时,我在2012年使用debuggerdisplay获得了这个结果:结构:bar,函数计算超时,函数计算由于上一个函数计算超时而被禁用。必须继续执行才能重新启用函数计算。
- 2015年在DebuggerDisplay工作得更好。如果2012将类型标记为"cannot use debuggerdisplay,give‘has disabled’error message",2015确实为我的第三个实例重新评估了debuggerdisplay。2015年给了我struct:bar,评估超时,struct:bar。
- 我的问题是为什么实例会触发这个。当然,这种ToString方法并不需要很长时间来评估。换句话说,必须有其他原因,而不仅仅是"1秒超时"触发了恢复到类型的这种行为。我想这就是为什么你贴了这个:)是不是所有局部变量的总和不能超过1秒?测试…
- @确切地说-这绝对不仅仅是一个暂停。(注意属性如何使这两个行为有所不同。)我简单地认为这可能是由于使用锁的评估造成的,但似乎不是这样。(在我看来,所有本地人的总数仍然很小。)
- Sum不是这样的。3个超时值为800的实例,如果我跨过了所有实例(在所有实例之前和之后都有断点),则计算结果都很好。把中间一个的超时时间提高到1200给了我结构:bar,demostruct,demostruct。
- 我试着在有或没有gc.collect的情况下一千次分配86000个字节,看看它是否跟踪内存使用情况或垃圾收集,但这并没有改变任何东西。
- 嗯。我在这件事上很不安。我已经将Thread.Sleep修改为60000,但是运行了调试器,它到达中断线的速度比一分钟快得多,并且总是显示ToString应该返回的自定义值。
- 嗯,发现了一些东西。如果在ToString中抛出异常,那么本地显示就是这种类型,即使根本没有睡眠。可以吗?请注意,如果启用代码分析,它将显式调用作为demostruct.toString()"创建类型为"exception"的异常在toString中引发的异常。不应在此类型的方法中引发异常。如果可能会引发此异常实例,请更改此方法的逻辑,使其不再引发异常。这是CA1065。
- 尝试将Visual Studio调试器作为黑盒进行调试当然是一个有趣的练习,但它真的是对您时间的有效利用吗?任何拥有源代码的人都可以很快为您的问题给出一个明确的答案,也许还可以解决这个问题。考虑放弃并使用这里建议的解决方法之一,或者(甚至)添加一些日志代码来实现相同的目标。
- @卡尔森:没有例外,只是有点奇怪。查看我的最新信息("Curiouser和Curiouser")。
- @迪奥米迪斯皮内利斯:好吧,我在这里问这个问题,是为了让a)以前见过同样的事情,或者知道vs内部的人能够回答;b)将来遇到同样问题的任何人都能很快得到答案。
- 我的3个实例的超时为8001200800+3个包装器,每个(使用完全相同的toString和完全相同的超时)显示struct:bar、2x type、3x struct:bar。是的,古玩者和古玩者。
- @乔斯基特说得对!或者甚至c)发现这是一个已知的错误,但没有很好的答案。
- @jonskeet InstantWrapper是一个类,Instant是一个结构。也许它对类和结构的评估不同?您也可以尝试将InstanceWrapper更改为结构。
- @埃伦斯&246;NMEZ:哦,很有眼力。是的,这改变了事情。更新。
- @乔恩有这么多评论,我都无法理解。你有没有试着看看评估的限制是否是方法大小?与考虑衬里方法时施加的限制类似?此外,结构和类之间的区别是否与方法调用IL有关?不确定它现在看起来是什么样子,我假设它是相同的,但也许编译器正在做一些额外的工作,以使值类型返回到object中。
- @Adamhouldsworth:我没有看,但是Instant.ToString方法的主体只是一个方法调用,所以我非常怀疑这是否相关。(不过考虑得不错。)
- 实际上,我的观察都是在demostruct是一个类的地方完成的,因为它是这个问题的第一个版本。我有点觉得奇怪,你给一个类命名为demostruct,但认为它只是一个繁重实验的产物,没有注意到你编辑了这个问题并改变了这个问题。所以我上面关于超时和3个实例和包装器之类的评论,一切都是类。
- @卡尔森:哎呀-是的,我注意到了并解决了这个问题,但没有把它提起来……你能很容易地检查观察结果是否仍然有效吗?
- @jon除非编译后的方法在方法调用的行内容中具有特性?也许IL比C大,你可以看到。抓紧稻草真的,迫不及待地想看到一个vs dev回答这个问题:—)
- 它改变了。对于类,3个实例+3个包装器(按顺序),超时为800、1200、800:ToString+2x type+3x ToString。将内部类更改为结构:4x ToString+2x类型。将包装器也更改为结构:6x ToString。这是一个代码,它只构造所有6个实例(3个内部实例,然后3个包装器),然后命中一个断点。所以,规则似乎是,如果它是一个结构,我们将对它进行评估,不管它是什么,但是对于类,有一个超时。好吧,除了包装纸,它的行为很奇怪:)我是随机投票的。这是江户十一〔0〕换了个新的伪装:)
- @卡尔森说,这条规则听起来像是基于struct类型(即无)与类的可预测继承链,这可能非常复杂和昂贵。我想知道这是否与过多的代码、跨库的代码或者其他什么有关。
- 也可能有一种期望,即结构应该是轻量级的值,而类往往更复杂。
- @乔恩说得更严肃一点,所以开发人员需要修复用户界面。您的徽章计数被剪切到用户磁贴上…
- @卡尔森:这很奇怪,不适合立刻成为一个结构。不过,我刚刚发现它变得更奇怪了——如果我在ToString中有10秒钟的睡眠,那么在评估它时,它似乎被忽略了。你认为值得我把这改回课堂,让问题更加一致吗?
- @卡尔森:已经更新了第一个同时具有结构和类的repro应用程序。令人恼火的是,今天早上(我第一次试图反驳这一点时),我已经有了DemoStruct和DemoClass,但DemoStruct始终是一门课。骗子我吸。
- @乔恩斯基特——我所说的,江户十一号(5号)似乎没有任何影响,就在那里。
- @不信的达米安:是吗?大概你把它当成了一个结构?当它是一门课的时候,确实会有不同。
- 我在VS2012中也有同样的功能,但从未找到解决方案。
更新:
此错误已在Visual Studio 2015更新2中修复。如果使用更新2或更高版本评估结构值的字符串时仍遇到问题,请通知我。
原始答案:
您正在使用Visual Studio 2015遇到已知的bug/设计限制,并调用结构类型上的ToString。这也可以在处理System.DateTimeSpan时观察到。System.DateTimeSpan.ToString()在Visual Studio 2013的评估窗口中工作,但不总是在2015年工作。
如果你对低层次的细节感兴趣,下面是发生的事情:
为了评估ToString,调试器执行所谓的"函数评估"。在大大简化的术语中,调试器挂起进程中除当前线程以外的所有线程,将当前线程的上下文更改为ToString函数,设置隐藏的保护断点,然后允许进程继续。当命中保护断点时,调试器会将进程恢复到以前的状态,并使用函数的返回值填充窗口。
为了支持lambda表达式,我们必须在Visual Studio 2015中完全重写clr表达式计算器。在高层次上,实施是:
Roslyn为表达式/局部变量生成msil代码,以获取要在各种检查窗口中显示的值。
调试器解释IL以获得结果。
如果有任何"调用"指令,调试器将执行如上所述的功能评估。
调试器/Roslyn获取此结果并将其格式化为向用户显示的树状视图。
由于执行了IL,调试器总是处理"real"和"fake"值的复杂混合。实际值实际上存在于正在调试的进程中。伪值只存在于调试器进程中。为了实现正确的结构语义,在将结构值推送到IL堆栈时,调试器总是需要复制该值。复制的值不再是"实际"值,现在只存在于调试器进程中。这意味着如果我们以后需要对ToString进行功能评估,我们就不能这样做了,因为这个过程中没有这个值。为了尝试获得价值,我们需要模拟ToString方法的执行。虽然我们可以模仿一些东西,但是有很多限制。例如,我们不能模拟本机代码,也不能执行对"真实"委托值的调用或对反射值的调用。
考虑到所有这些,下面是导致您看到的各种行为的原因:
调试器没有评估NodaTime.Instant.ToString->这是因为它是结构类型,ToString的实现不能如上文所述,由调试器模拟。
当ToString调用struct->这是因为仿真器正在执行ToString。thread.sleep是一个本机方法,但模拟器知道而忽略了这个电话。我们这样做是为了获得价值向用户显示。在这种情况下,延迟是没有帮助的。
DisplayAttibute("ToString()")工程。->那太令人困惑了。唯一ToString的隐式调用与DebuggerDisplay是隐式ToString的任何超时。评估将禁用所有隐含的ToString评估。键入直到下一个调试会话。你可能在观察行为。
在设计问题/缺陷方面,我们计划在未来的Visual Studio版本中解决这一问题。
希望能把事情弄清楚。如果有更多问题,请告诉我。-)
- 如果实现只是"返回字符串文本",那么您知道instant.toString是如何工作的吗?听起来有些复杂的事情还没有被考虑到:)我会检查我是否真的能重现那种行为…
- @乔恩,我不知道你在问什么。调试器在进行实际的函数评估时对实现是不可知的,它总是先尝试这一点。调试器只关心需要模拟调用时的实现——返回字符串是要模拟的最简单的情况。
- 我有点头晕目眩,但字符串文本不是由编译器硬编码的吗?在这种情况下,调试引擎可能知道不需要进行任何计算,只需从存储字符串的任何位置获取该字符串。
- @patricknelson msft:如果我用return"Hello";替换ToString()的实现,那么调试器会显示它。我认为这将是一个调试器模拟对ToString()的调用的情况-如果它实际上是模拟ToString()的内容,我想这可能是不同的…
- 理想情况下,我们希望clr执行所有操作。这提供了最准确和可靠的结果。这就是为什么我们要对ToString调用进行真正的函数评估。当这不可能的时候,我们就回到模仿这个电话上。这意味着调试器假装是执行该方法的CLR。显然,如果实现是return"Hello",那么这很容易做到。如果实现是P-invoke,那么就更难或不可能了。
- 嗯,关于线程。睡眠:也许是个很愚蠢的例子,但是像这样的东西呢——while (innerResult == null) Thread.Sleep(10); return innerResult.ToString();。模拟器是否知道该线程。在该场景中不应忽略睡眠?
- @Tzachs,模拟器是完全单线程的。如果innerResult开始时为空,则循环将永远不会终止,最终评估将超时。实际上,默认情况下,计算只允许进程中的单个线程运行,因此无论是否使用模拟器,您都将看到相同的行为。
- 顺便说一句,如果您知道您的评估需要多个线程,请查看debugger.notifyofCrossThreadDependency。调用此方法将中止计算,并显示一条消息,说明计算需要运行所有线程,调试器将提供一个按钮,用户可以按此按钮强制计算。缺点是,在计算过程中,任何命中其他线程的断点都将被忽略。
- @Patricknelson MSFT能否解释一下你所说的"real" delegate values和calls on reflection values是什么意思?后者是否引用使用Activator.CreateInstance创建的对象?
- 实际值存在于正在调试的进程中。假值则不然。例如,如果您评估类似于F(() => new A()) + b的内容。new A()的结果将是一个假值,因为仿真器创建了它。如果"b"是一个局部变量,它是一个实值。反射值是生活在System.Reflection和类System.Type中的类型的对象。
- @Patricknelson MSFT,代码仿真是否发生在另一个CLR实例中?或者仿真器是纯本地的?
- 调试器能否利用NodaTime.Instant是不变的这一事实?
- 这一问题将在2017年的对比中下降。我有155.2