October 16th, 2023

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

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().”

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

9 comments

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

  • Mohammad Ghasemi

    What design decisions can lead to violation of this principle?

  • GL

    >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...

    Read more
    • Raymond ChenMicrosoft employee Author

      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.

    • Ron Parker

      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...

      Read more
  • Shy Shalom

    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

      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#...

      Read more
  • Antonio Rodríguez · Edited

    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...

    Read more
    • Ron Parker

      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...

      Read more
      • Antonio Rodríguez · Edited

        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...

        Read more