不多说了,都是泪。
最近3个月这种现象前前后后出现好几次了,今天又排查了一个这个问题:
程序崩溃,通过Debug来排查问题,各种断点挨个打上,Step Step Step …… 成功Pass。
然后依次减少断点个数,继续Debug,每次都能成功Pass。
直到所有断点取消,直接运行,程序崩溃。
???????
我模拟了一个这个现象,大家可以在下面的程序的Bomber
类的GetReady()
函数中打上断点,只要程序运行到断点,在局部变量中展开这个Bomber实例类,即可正常运行。取消断点,则程序会报错(“BOOOOMMMMMM!!!”):
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApp1
{
public class Bomber
{
List<bool> _triggers;
private int _someTrivialIntegerValueToTrickCompiler;
public Bomber(int count)
{
_triggers = Enumerable.Repeat(true, count).ToList();
_someTrivialIntegerValueToTrickCompiler = 0;
}
public bool KeyPointProperty
{
get
{
Random rd = new Random();
int max = _triggers.Count - 1;
if (_triggers.All(t => t))
{
_triggers[rd.Next(0, max)] = false;
}
return _triggers[rd.Next(0, max)] || _someTrivialIntegerValueToTrickCompiler > 0;
}
}
public void GetReady()
{
_someTrivialIntegerValueToTrickCompiler = 1;
}
public void Boooom()
{
if (_triggers.All(t => t))
throw new Exception("BOOOOMMMMMM!!!!");
}
}
class Program
{
static void Main(string[] args)
{
var boom = new Bomber(100);
boom.GetReady();
boom.Boooom();
}
}
}
下面的动图展现了实况…
下面笔者就展开来讲讲到底怎么回事…
解答
在上面的例子中,导致这种奇特现象的出现的原因就是一个动作:
在局部变量中展开这个Bomber实例类
由于在类Bomber
中,包含属性KeyPointProperty
,所以在Visual Studio Debugger中的局部变量 观察 该类的实例时,会尝试使用这个属性的get
方法来获取该属性值,此时就会运行get
方法。然而这个属性的get
方法里面进行了一番操作,改变了触发throw exception
的条件:
public bool KeyPointProperty
{
get
{
Random rd = new Random();
int max = _triggers.Count - 1;
if (_triggers.All(t => t))
{
_triggers[rd.Next(0, max)] = false; // 改变了触发条件,使得_triggers不都是true值了
}
return _triggers[rd.Next(0, max)] || _someTrivialIntegerValueToTrickCompiler > 0;
}
}
此时,在断点之后点击继续,由于_triggers
属性值已经改变,因此就不会触发exception
了。
结论
由于 Debugger尝试获取某个类的实例的某个属性,从而使得这个属性的get
方法被执行,导致了某些不可预计的 条件/变量改变,进而导致本应该复现Bug的测试用例正常运行,造成不会出现错误的假象;而当我们常规运行程序的时候,由于该属性的get
方法并未被执行,则程序会报错。
下次遇到了这种奇怪的现象,不如查查相关的类的属性是不是因为Debugger的介入,导致了一些自己看不见的改变呢?
🦀
–
题外话
本来文章就应该到这里结束了,但是坑仅仅就在这里结束了吗?在笔者自己写该文章的过程中,曾经试图用一个更简单的案例来尝试复现这个bug,但是却没有成功:
namespace DebuggerCausedBugMissing { class Alpha { bool _trigger = true; public bool TriggerProperty { get { _trigger = false; return true; } } public void Elephant() { if (_trigger) throw new Exception("BOOMMM"); else Console.WriteLine("Passed!"); } } class Program { static void Main(string[] args) { var alpha = new Alpha(); Console.WriteLine("Debugger stepping in."); alpha.Elephant(); } } }
上例中,如果在创建
Alpha
实例之后打断点,对该实例进行观察,则这个“观察”行为虽然使用了属性的get
方法来获取到true
这个返回值,但却神奇地并没有改变Alpha
实例的_trigger
字段的值。
重申一遍:
TriggerProperty
这个属性的get
方法被调用,但却没有改变_trigger
字段的值。
这是怎么回事,难道Visual Studio Debugger使用了TriggerProperty
的另外一个版本get
?它自己会生成一个“简化版”的get
用来观测局部变量?笔者并没有得出结论。
笔者虽然不知道为什么这种简单直接的
get
属性没触发实际上框架设计中带来的问题,但是笔者发现只要给这个属性套一层List
的壳子,这个问题就会被正常触发了:class Alpha { List<bool> _trigger = new List<bool> { true, true }; public bool TriggerProperty { get { _trigger[0] = false; return true; } } public void Elephant() { if (_trigger[0]) throw new Exception("BOOMMM"); else Console.WriteLine("Passed!"); } }
这种情况下,前文叙述的断点引发的血案就会发生。
这一切到底是为什么,可能笔者以后会找到这个问题的答案吧。