【C#杂谈】一个有趣的Debug经历 —— 打个断点程序就能正常跑,取消断点程序就崩溃

不多说了,都是泪。

最近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!");
  }
}

这种情况下,前文叙述的断点引发的血案就会发生。
这一切到底是为什么,可能笔者以后会找到这个问题的答案吧。

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值