API design principle: Reading a property or adding an event handler should not alter observable behavior

Raymond Chen

On of the Windows Runtime API design principles is that reading a property¹ or adding an empty event handler should not affect the API’s proper usage. It is legal for the implementation to optimize based on whether a property was accessed or whether a handler is registered,² but the optimization should not affect overall correctness.

Here are examples of bad behavior we want to avoid:

If you read the Widget.Stream property, you must call the Close method on the returned stream.

If you add a handler for the FancyReady event, then the PlainReady event is not raised.

The MischiefDetected event handler must call MischiefManaged before returning.

The reason for the “reading a property should not affect proper usage” guideline is that many debuggers will “helpfully” dump the properties of an object. In the case of the Stream property above, if reading the Stream creates an obligation to Close it, then each time you hover over a widget or log it to the console, the debugger will read the Stream property and show it on the screen. The debugger doesn’t know the special rule about having to Close the stream, so the stream will go unclosed, and you have a memory leak.

Even worse, that stream may be associated with an open file handle, so now you leaked a file handle, and the effects of a leaked file handle can be quite severe. Debugging is hard enough. Don’t create a situation where a bug is introduced by the presence of a debugger. “Yeah, we can’t run this program under the debugger to figure out what is going wrong, because once we run it under the debugger, it crashes with a sharing violation.”

It is also common, especially when learning how to use a new feature, to add handlers to every event, where all the handler does is log a message like “FancyReady received” followed by the values of all of the event arguments. This lets developers see the event flow and gain a better mental model of how the feature works. But if adding an event handler changes the feature’s proper behavior, you create a version of the Heisenberg Uncertainty Principle: Attempting to observe the system changes its behavior.

And you definitely don’t want to put people into a position where they throw up their hands in frustration and say, “I don’t understand. Once we connect a debugger or turn on logging, the program crashes even before we get to the problem we’re trying to solve. This problem is un-debuggable.”

¹ You are allowed to raise an exception from a property access if the situation calls for it.

² You are allowed to require that a handler be registered for an event. That doesn’t violate the principle, because you’re saying that omitting the handler is was never proper API usage to begin with. (In C/C++ terms, it is “undefined behavior”.) It does mean that if the developer adds a dummy handler that just logs information but does no work, they might inadvertently “fix” their program. In the case of improper usage, you should pass a custom message to Ro­Originate­Error to remind the developer why the operation failed. “You must register a MuffinReady handler before you can Bake().”

9 comments

Discussion is closed. Login to edit/delete existing comments.

  • Antonio Rodríguez 0

    You are talking about the consequences of those side effects while a debugger is active, or when you are learning the API. Which is fine. But I think there is a more fundamental justification for this principle.

    A property is a pair of getter and setter in disguise which makes it look and act as if it were a member variable. Thus, just as reading a variable should have no side effects at all, reading a property (and calling the underlying getter) shouldn’t have them, either. Maybe the getter takes its time to calculate the requested value, or maybe it needs to initialize a cache which takes some memory and has to be cleared on destruction, but there should be no change in the object’s publicly observable state.

    The same can be said about events. You can see them as notifications or messages of some kind. The IRS is telling that you are late with your income tax, but if you already know, you can toss the letter to the bin. You decide (and assume the consequences!). Subscribing to an event or ignoring it should make no difference. Possibly with the exception of mandatory events, or those with well-documented by reference parameters (like the Cancel parameter in classic Visual Basic’s QueryUnload and Unload events).

    If it is designed to be yellow, have feathers and quack, you better make sure it behaves like a duck. Don’t make promises you won’t fulfill.

    • Ron Parker 0

      These days, it’s mostly something you run into with embedded systems, but there’s a long tradition of hardware registers that are reset-on-read. They generally serve a specific purpose along the lines of “what has changed since the last time I asked?” which turns out to be a convenient way to avoid the potential race condition that happens when read and reset are separate operations and there’s no such thing as locking.

      I agree that it’s probably bad design to make such registers properties of some theoretical abstraction layer, but I can definitely see it happening. Especially if the abstraction layer was originally written for some similar piece of hardware that didn’t do reset-on-read and the API is already defined. (Of course, in that case you’re going to spend the next two years tracking down the bugs caused by the behavioral difference, so you should just change the API anyway, but you know everyone’s done it the “easy” way at least once.)

      • Antonio Rodríguez 0

        Oh, yes. Hardware registers. Lovely stuff, filled with features whose only purpose seems to cause trouble. But those have the excuse of being one-off interfaces, often used just by the driver provided by the maker. Anyway, even when I’m designing an interface just for my use, I tend to avoid those gotchas. I have been bitten enough times. In fact, there is this user interface design adage, “if the user needs to look at the manual, you have lost the game”, which also applies to software interfaces.

  • Shy Shalom 0

    C# has one of these baked into the language.
    When an event has no subscribers, it is equal to null, so you need to have a null check before invoking the event, which introduces a flow divergence from the case where you do have subscribers.

    • GL 0

      You cannot invoke an event — what people are often doing is to invoke the delegate stored in the underlying field of a field-like event, which is internal to the class, as the implementation of raising that event. The principle does not suggest that C# should ensure a field-like event is backed by a non-null delegate. Instead, it means that the implementing class must raise the event correctly with the null check, given how C# implements it.

  • GL 0

    >If you read the Widget.Stream property, you must call the Close method on the returned stream.

    This slightly confuses me. In WinRT (or COM in general), if you get a property of object type (whose return type is pointer to IUnknown or its descendant), then you’re of course supposed to Release it. I suppose the principle does not apply to this case, as any decent debugger should be aware of this situation. (Also a property returning a string needs to be properly deallocated.)

    Now here’s the confusion part. If you read Stream and the returned value is the last reference to it, then I expect Releasing it will cause the stream to be Closed. If not, then I’m not sure if Closing it is a good idea because others might read from the property, only to get a closed stream. I get the idea of this principle, but it’s difficult for me to come up with a natural scenario where getting such a property would require (by how it is naturally coded) the caller to Close it.

    • Ron Parker 0

      Perhaps you’re working on a legacy system that has a vast C API, and Widget.Stream is a property provided for backward compatibility with that C API – it actually returns a “stream handle” that’s just a bare pointer to some opaque structure plopped into a chunk of memory that the property getter allocated just for you. To work with it, you use a bunch of C functions, and when you’re done, you close it by calling one of those C functions.

      Windows still has a lot of APIs like this, and even some COM APIs like this (CoTaskMemFree, anyone?) There’s a whole chunk of WIL whose entire purpose is to make managing stuff like this less error-prone. The goal is for WinRT to avoid going down the same road.

    • Raymond ChenMicrosoft employee 0

      It happens when people implement the Stream getter as IStream Stream { get { return new Stream(); } }. They aren’t thinking of a property as “tell me the value of this thing”, but rather as a handy syntax for function calls.

  • Mohammad Ghasemi 0

    What design decisions can lead to violation of this principle?

Feedback usabilla icon