Debugging When Property Reads Have Side Effects

The last couple of days, I've been working with some code where there was a side effect when a particular property was read. This made it more cumbersome to debug the code, until a very simple solution dawned on me. It's so stupidly simple that I suspect I'm the last person to ever discover it, but I have never heard or read about it, so here it is:

Imagine a class where there's a side effect every time a property is read:

 public class MyClass
{
    private int number_;
 
    public int Number
    {
        get { return this.number_++; }
    }
}

As usual, the example is patently stupid, but it illustrates the point that each time the Number property is read, this.number_ is incremented, so the next time the property is read, the return value is one higher - I'm sure you get the point.

Imagine, then, that you consume this class the following way:

 MyClass mc = new MyClass();
int i = mc.Number;

What would you expect the value of i to be? My answer is 0, and if you run an application where this code appears, that is also the case, but something funky happens if you set a breakpoint on the second line and debug the application. The Autos window will show the value of mc.Number to be 0, but when you move on, a value of 1 will be assigned to i, possibly making your software behaving differently while in debug mode. What's going on?

While it is no secret, it seems that most developers are not really aware of the fact that the debugger displays values of properties by invoking them. In this example, as soon as you hit the breakpoint, the debugger invokes the Number property to be able to show the value in the Autos window, and this obviously increments the value. When the code execution resumes, the Number property is invoked again, and the result is now 1, which is assigned to i.

You can even play around with this while the debugger breaks at the breakpoint. Notice that the value in the Autos window displays 0 for the Number property. Then hover your mouse pointer over the Number property in the code and notice that the tooltip-like debugger display shows a value of 1. Remove the pointer and return to the Number property once more: Each time you do this, the debugger invokes the property, so the value will increase every time.

This obviously makes it a bit difficult to debug code with such behavior, so what's my simple solution? Simply turn off the Autos window! Typically, the Autos window is grouped together with some Watch windows, so simply replace the Autos window with a Watch window that doesn't include the troublesome property, and you can debug away to your heart's content - as long as you also remember not to let your mouse pointer hover over the property.

When you are done working with the troublesome property, you can always switch the Autos window back on.

If you were only interested in the tip, you can stop reading now.

On the other hand, you may think that it's bad API design in the first place to have a property where a simple read operation has side effects on the state of the object, and in general I'd tend to agree.

First of all, if an operation has side effects, it should be a method instead of a property, and even with methods, good API design recommends that a method should preferrably return a value, or change the state of the object, but not both. Even so, there's a reason why we also recommend developers to encapsulate fields in properties, instead of just exposing public fields: By encapsulating fields in properties, you keep open the possibility that you can later change or extend the behavior of the property, so a simple field-backed property could over time evolve into something where a read operation has side effects.

Secondly, even if you are not convinced by the above argument, you may inherit code with this behavior from someone else, and you may need to debug it.

In real production code, the side effect may be far more sophisticated and less obvious than my example; the original developer may not even realize that this is the case, and the side effect may only occur if certain other conditions are satisfied.

This situation also arises if you are using dynamic mocks with strict expectations: If you are mocking an interface with a property, each time this property value is displayed by the debugger, an expected read on the mock object is used, so when you continue code execution, the mock will typically fail because it's getting more reads than expected.