I’ve been subscribed to the Los Techies RSS feed for a few months. I find that I agree with a lot of the bloggers posting there and enjoy learning from their insights into design and methodologies. For instance, today Jimmy Bogard wrote about the perils of downcasting and how breaking the open-closed principle by making assumptions about your parameters’ lineage can limit the ability of your code to adapt to change.
One small detail in the post stood out at me because it reminded me of a past design decision.
“To make things worse, it explicitly casts instead of using the C# “as” operator (TryCast for VB.NET folks). At least then it won’t fail when trying to downcast, it would just be null. “
On a previous project I recall when we excavated the “as” operator from the C# documentation. Normally, when you perform an explicit cast in your code, casting to a type that the object to be casted cannot be cast to throws an InvalidCastException.
Walrus bucketLover;
Cat tabbyCat = (Cat) bucketLover; // throws InvalidCastException since a Walrus is not necessarily a Cat
The “as” keyword instead returns null in the case that you have cast an Walrus to something as ridiculous as a Cat. When you use the “as” keyword you can cast indiscriminately and completely throw caution to the wind.
Walrus bucketLover;
Cat tabbyCat = bucketLover as Cat; // sets tabbyCat to null. No exception is thrown..
This seems like a good thing, right? Throwing exceptions is bad. Conversely, not throwing exceptions is good. We made the decision to always use “as” instead of an explicit cast. All seemed well… except that trouble was brewing. If tabbyCat could now be null, then we needed to check for null immediately after the cast. After all, if tabbyCat was null then we couldn’t continue execution of the code in that method. A basic assumption that the method depended on, that an argument needed to be of a particular type, was not met and thus we could go no further on.
Clearly, we needed a way to tell a higher level of code unambiguously that an assertion had failed and we needed to handle it in some way. That sounded like an exception to me. Otherwise, code further downstream had a chance of getting a null tabbyCat object, and it might not be immediately obvious that the reason tabbyCat was null was that a cast had failed.
Walrus bucketLover;
Cat tabbyCat = bucketLover as Cat;
if ( bucketLover == null ) throw new InvalidCastException();
Hmmm. If only the code could do this more succinctly. Wait, one quick refactoring later and:
Walrus bucketLover;
Cat tabbyCat = (Cat) bucketLover;
That code snippet may seem familiar. It’s what we started with. The lesson I learned here was that guarding execution with explicit casting wasn’t always the wrong decision. Letting the cast throw an exception when assumptions weren’t met allows the code to be more succinct. It allows the code to be more concise because you don’t have to spend a lot of time checking for nulls everywhere, and you don’t have to worry about tracking down the source of a NullReferenceException.
I realize that using exceptions versus using return values for everything is a religious debate, and I’m not trying to say any approach is morally and unambiguously right or wrong. What it does allow you to do is have your code be more focused on its particular work and less on the nuts and bolts of the underlying language.
I think we can all still agree that downcasting is evil.