今天我遇到了一个幽灵方法

今天我遇到了一个幽灵方法,诡异至极,虽然我确信这个方法没有被调用过,无论是通过打断点,还是添加输出代码,我都在运行时验证了这一点,但诡异的是,那些只受该方法影响的变量,就是发生了改变。所以这是一个没有被调用,却又明显调用过的方法,一个幽灵方法。
这个方法大概是这样的:

public class Weight {
    ......
    public virtual Weight setValue(double weight) {
            isSet = true;
            nextWeight1 = null;
            nextWeight2 = null;
            this.trueWeight = (float)weight;
            return this;
        }
}

该方法会在值被设置的时候,做一些清理工作。
但出现幽灵现象的其实不是这个方法本身,问题发生在Weight的子类BiWeight身上。在我的逻辑中BiWeight的对象是没有机会调用到这个方法的,因此我也就没有重载这个方法在BiWeight中的实现。但最近我在调试中,需要查看BiWeight的内部变量的值的时候,却发现了一些诡异的现象。

调试时看到的内存状态

我检查了代码,确认唯有在这个方法中,isSet才有可能会被设为true,因此这个方法千真万确地被调用过。

为了确定这一点,我在BiWeight中重载了这个方法,并加了断点。

运行,断点没有被触发。我怀疑可能是被编译器优化掉了。

于是我添加了输出语句。

运行,没有相应的输出。
这么看来,这个方法应该没有被调用才对。
于是我有修改了实现,不再调用父类的方法。


运行,于是诡异的一幕发生了。


修改实现后对象的内存状态

内存状态确实变化了。

但是一个没有被调用的方法是如何影响到对象的内存状态的呢!!!

难道遇到鬼了?我写了这么多年的代码,撞见鬼打墙了?为什么会存在一个这样的幽灵方法呢?

在随后的一两个小时里,我对此进行了反复测试,测试结果仿佛指向了一个规律,对象的内存状态在告诉我,幽灵方法确实被调用了,而程序的外部输出又在告诉我另一个事实,幽灵方法并没有被调用。可是为什么呢?为什么程序会有内外两种表现?

身心俱疲的我陷入了绝境。

忽然,我注意到了内存状态中的一个展示项:TrueWeight。


TrueWeight是一个属性

TrueWeight是一个属性!!!

那一刻,我仿佛听见一个遥远的声音传来,真相只有一个!!!

在这个程序中,TrueWeight属性并不是对trueWeight的简单封装,他还有一些额外的逻辑:

    public double TrueWeight {
            get {
                if (!isSet) {
                    if (nextWeight1 != null) {
                        setValue(nextWeight1.TrueWeight);
                    } else if (nextWeight2 != null) {
                        setValue(nextWeight2.TrueWeight);
                    }
                    setValue(infinitesimal);
                }
                return trueWeight;
            }
        }

我想,在C#编程实践中,在对属性的访问时,会执行一些附加的逻辑这种做法应该是相当普通的现象。而当我在查看对象的内存状态时,IDE同时也把对象附带的属性也显示出来。那么当IDE去访问属性时,会不会也触发了这些逻辑呢?

为了验证的我想法,我编写了一个简单的程序。

    public class TestClass {

        public int v = 0;
        bool isSet = false;

        public int TestPropety {
            get {
                if (!isSet) {
                    isSet = true;
                    setValue(1);
                }
                return v;
            }
        }

        void setValue(int i) {
            Console.WriteLine($"setValu({i})");
            v = i;
        }

        public static void Main() {
            TestClass test = new TestClass();
            Console.WriteLine($"value = {test.v}");
        }
    }

直接运行该程序的结果是这样的:

随后,我给程序打了断点:

运行,程序如预料之中的那样停在了Main方法体内的断点处,此时,我查看了一下test对象的内存状态:

setValue方法体内的断点没有触发,好像什么都没有发生一样。但v的值确实发生了变化。

我放开断点,继续运行,程序的输出如下:

我看到了setValue方法内的输出。

现在程序究竟发生了什么已经昭然若揭了。

在测试程序的自然运行过程中,setValue方法确实没有被执行,因此相关的输出是不可见的。但当我在程序运行过程中去查看对象的内存状态时,IDE会收集对象的字段和属性的值以供显示,而这一操作会触发属性的内部逻辑,从而导致对象的状态被修改,甚至影响到了程序的最终结果。换句话说,我的调试行为影响到了程序的行为。

在我的认知里,我一直认为C#的属性和Java的set/get方法是差不多的,无非是属性要更强大一些,能够做一些set/get方法做不到的事情。但现在看,属性有点儿强大过头,最起码,Java的任何IDE都不会在你查看内存状态的时候,把set/get方法的返回值也一并显示出来,并牵一发而动全身地做了一些你意想不到的事情。当然,事实上,问题不在于C#的属性设计得有问题,至少在程序自然运行状态下,一切都是自然,妥帖的。存在问题的是IDE的行为,在明知道收集属性的值会触发属性中包含的逻辑的情况下,还这么做,不是傻就是坏。

这种结果带有巨大的迷惑性,出现问题的地方和导致问题的地方相距甚远,而且无声无息,说不好哪天就会因此而栽跟头而毫不知情。你可能会疑惑,测试程序中的属性输出明明可以看见,为什么我之前却说没有看见?但那是在已知调试行为会影响程序运行的前提下,我才能够看见,换言之,我必须在查看了对象的内存状态之后再去查看输出才行,如果没有这个意识,或者没有依据这个顺序,都是无法看到输出的。而且,大多数情况下,我们都不会在访问属性时输出信息,也就意味着,即便出现问题,我们基本是无从得知的。这个情况要是再和程序中存在的错误逻辑叠加在一起,后果绝对是灾难性的,如果我们不知道我们调试的本身可能会影响到程序的行为,我们可能永远都搞不清楚问题到底出在哪里。

如果IDE坚持这么做的话, 那么我对大家的建议是,尽量不要在属性中附带可能修改对象内存状态的逻辑,或者如非必要,尽可能使用set/get方法来替代属性。但如果我们这么做,属性的价值又何在呢?鸡肋吗?

最后,我的程序并没有bug,真是瞎耽误工夫~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,340评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,810评论 25 707
  • 有些人,可能很大一部分人(包括自己),茌渴望成长的旅途上,都走过这样的弯路:买了很多书,也读了很多书;订了很多课程...
    _德成阅读 309评论 1 1
  • 感谢我们的祖先给后人留下了七月十五这个传统节日,更感谢我们的父母养育、教育了我们。我真诚地希望普天下的儿女都懂得呵...
    愛月亮的魚兒爱阅读 213评论 0 0