NULL as a function argument should be illegal
I’m going to confess a realization that I’ve come to regarding one of my oldest and strongest API-designing habits—that I now believe that habit to be a mistake.
Whenever I design an API, my mind habitually wanders to how its functions should handle NULL for various arguments. Usually I make NULL do something special. In this way, I give NULL the implication of “do this operation with what you already have”, or “do this with the default value for this argument”.
This is a mistake. NULL should be an error.
There are two reasons why this is so:
-
Clarity
Compare these two pieces of code*:
remove_filter(NULL, NULL);
remove_all_filters();
Which is clearer? Which makes the statement’s action more explicit?
-
Robustness
What happens when (not if) somebody passes NULL by accident?
Consider this:
hookName = [filterNameField stringValue];
[self removeFilterForHook:hookName
target:nil
selector:NULL];What happens when you forget to connect the filterNameField outlet in Interface Builder, or when one of your localizers accidentally disconnects it, or when you or another programmer on the project deletes filterNameField, replaces it with something else, or cuts-and-pastes it (e.g. into a new tab view item), and fails to reconnect it?
When the outlet is not connected, filterNameField will at runtime be nil. Messages to nil return nil, so hookName is assigned nil. Then you pass this value (nil) as the hook name to removeFilterForHook:target:selector:**. Oops—you just removed all the filters.
The solution is to add a new method called removeAllFiltersForHook:, and make nil/NULL illegal in all four arguments (the one to removeAllFiltersForHook: and the three to removeFilterForHook:target:selector:). Thus:
hookName = [filterNameField stringValue]; //stringValue returns nil, as before
[self removeAllFiltersForHook:hookName]; //Assertion failureRather than removing those filters and causing invalid (unfiltered) data to pass through, which will hopefully be noticed now but Murphy’s Law says will be noticed much later (after you have forgotten this code), the new code comes to a screeching halt with a message in the console. If your program is seriously robust, your crash-catcher (for you do have one) will catch the exception and present a crash-report dialog.
And, of course, your code is much clearer—no longer must the maintenance programmer (you, in six months) think about and interpret what you meant by “target:nil selector:NULL”, as such magic has been banished.
Counterpoint, and the rebuttal to the counterpoint
The main reason why I’ve made functions accept NULL and handle it specially is so that I can implement some higher-level method (such as removeAllFiltersForHook:) with it. For example, a function in Growl’s Carbon API:
Growl_RegisterWithDictionary
Register your application with Growl without setting a delegate.
Boolean Growl_RegisterWithDictionary(CFDictionaryRef dict);
When you call this function with a dictionary, GrowlApplicationBridge registers your application using that dictionary. If you pass
NULL
, GrowlApplicationBridge will ask the delegate (if there is one) for a dictionary, and if that doesn’t work, it will look in your application’s bundle for an auto-discoverable plist.
<rant>
Now, I ask you: What purpose is there for this magic NULL behavior? Why would you set a delegate (which the framework asks for a registration dictionary immediately), then explicitly register with no dictionary? What would you expect that to do?
This flagrantly violates the principle of UI design of “if you had to document it, it wasn’t designed intuitively”. The same principle ought to apply to API design. There’s already a Growl_Reregister function which you can use if your delegate’s registration dictionary changes, or if you didn’t have one and you just created it. There’s no reason why you should be able to call Growl_RegisterWithDictionary with no dictionary (for that’s exactly what passing NULL is) and have it do the same thing. What it should do is fail an assertion or something.
(For the record, I’m not calling out anybody else on the Growl Project on this. I designed that API, and I wrote the entirety of what I quoted above. All blame for this rests squarely with me.)
</rant>
The reason why I had this function act this way was so that Growl_Reregister could be a one-liner. Here’s the code (Growl BSD license):
void Growl_Reregister(void) {
Growl_RegisterWithDictionary(NULL);
}
Convenient. In turn, Growl_RegisterWithDictionary does some set-up, then passes the registration dictionary (whether it was passed in, obtained from delegate, or otherwise produced or assembled) off to another, internal function. I wouldn’t want to duplicate the code of Growl_RegisterWithDictionary, right?
But I don’t have to. All the set-up code from Growl_RegisterWithDictionary does is:
- If a dictionary was passed in, make sure that that dictionary is completely filled-in (and if it isn’t, fill it in from the delegate’s dictionary). This is handled by another function—Growl_RegisterWithDictionary just calls that function.
- If a dictionary was not passed in, get a filled-in dictionary from one or more other places.
If we make Growl_RegisterWithDictionary assert that its dictionary is not NULL, the code changes to:
- Assert that a dictionary was passed in.
- Make sure that the dictionary is completely filled-in.
Neither of these would be duplicated in Growl_Reregister—it takes no arguments, and the dictionary that it gets from elsewhere either is already completely filled-in by the time Growl_Reregister receives it or is not usable anyway. More to the point, any code that is using Growl_RegisterWithDictionary(NULL)
would have to change to Growl_Reregister()
, making it both clearer and shorter.
Conclusion
NULL should not be magic. It should not be special. For the sake of clarity and robustness, It should be illegal.
* The functions are named this way because I was thinking about WordPress at the time, but this really has nothing to do with WP, and I don’t know if it is even relevant at all. That is to say, I don’t know whether WP’s remove_filter treats null specially in any way. Case in point. ↶
** Alert readers will note the similarity to NSNotificationCenter’s API. This is not intentional, but the similarity is real, and you should take from it that this situation is not merely theoretical: it certainly has happened, and will happen again. ↶
February 25th, 2007 at 08:39:07
I’ve definitely made this mistake with NSNotificationCenter before — observing for an object I “know” is non-NULL, then being confused when I receive notifications for everything because a buglet elsewhere had nulled the variable. Making it illegal is a great design idea.
February 25th, 2007 at 13:36:44
AIContactController is (or was, before I changed some of it) a particularly flagrant abuser of this principle. It definitely tends to conceal bugs, and imo NULL arguments should probably assert.
February 26th, 2007 at 10:26:59
I think you miss an important use of NULL, and one i believe to be valid. (example from NSXMLNode)
-(NSArray *)nodesForXPath:(NSString *)xpath error:(NSError **)error
When your methods are providing a way to return additional data into a user-specified memory location, being able to optionally say “no thanks, I don’t want that data” is useful, especially when following a standard model (like the NSError one). When it comes to using NULL/nil as a data providing argument, I think you’re right, however.
February 26th, 2007 at 10:36:39
Phil: Yup, I totally agree—for an output (return-something-by-reference) argument, using NULL to mean “no thanks” should always be allowed.
I’ll update the post later today, but I’ll point out now that this really only applies to C and its descendants. Other programming languages—those with a first-class sequence type—should simply return everything in a tuple/list/array and let the caller unpack the values and throw away what he doesn’t want. (Fictional Python example:
x, y = myView.frameOrigin()
)February 27th, 2007 at 08:54:20
So on a related note, what do you feel about assert()ing pre- and post-conditions for methods? Obviously the technique is a lot more powerful in languages like Eiffel where it can be used at compile time to disallow broken programs, but it still seems potentially useful in other languages.
March 9th, 2007 at 17:33:04
assert()
ing on illegal pre-conditions is extremely useful (although you need to get used to looking at the top of the method for them ;). I’m not sure how asserting on post-conditions would be useful within a method: shouldn’t you assert when you find an inconsistency at any point, not just post-flight?