views:

1273

answers:

5

My team has recently started using Lance Hunt's C# Coding Standards document as a starting point for consolidating our coding standards.

There is one item that we just don't understand the point of, can anyone here shed any light on it?

The item is number 77:

Always validate an enumeration variable or parameter value before consuming it. They may contain any value that the underlying Enum type (default int) supports.

Example:

public void Test(BookCategory cat)
{
if (Enum.IsDefined(typeof(BookCategory), cat))
{…}
}
+13  A: 

Enums are not checked:

enum Foo { A= 1, B = 2, C = 3}
Foo x = (Foo) 27; // works fine

Now you have a Foo that isn't defined. He is just saying "check your input". Note that Enum.IsDefined is relatively slow (reflection based). Personally, I tend to use a switch with a default that throws an exception:

switch(x) {
    case Foo.A: /* do something */ break;
    case Foo.B: /* do something */ break;
    case Foo.C: /* do something */ break;
    default: throw new ArgumentOutOfRangeException("x");
}
Marc Gravell
I agree that you should always have a default: throw new ArgumentOutOfRangeException. Also covers you for the scenario where a new value gets added to the enumeration.
Richard Ev
Arguably, NotSupportedException might be more appropriate - but heck, as long as it throws!
Marc Gravell
Agreed! Throw new OldShoeException() is better than letting the logic fall through and a bug showing up elsewhere as a consequence.
Richard Ev
Uh oh, you've been Skeeted! :-)
cletus
+1  A: 

This is because it's totally legal to call that "Test" method like this:

Test((BookCategory)-999);

That'll cast -999 as a BookCategory, but of course the result (passed as the "cat" parameter to Test) isn't a valid value for the BookCategory enum.

Matt Hamilton
I think you mean "That'll cast -999"...
Martinho Fernandes
Oops, you're right - I went back and edited one part of the post but forgot to change the other part. Thanks!
Matt Hamilton
+16  A: 

The point is that you might hope that by having a parameter of type BookCategory, you'd always have a meaningful book category. That isn't the case. I could call:

BookCategory weirdCategory = (BookCategory) 123456;
Test(weirdCategory);

If an enum is meant to represent a well-known set of values, code shouldn't be expected to sensibly handle a value outside that well-known set. The test checks whether the argument is appropriate first.

I'd personally reverse the logic though:

public void Test(BookCategory cat)
{
    if (!Enum.IsDefined(typeof(BookCategory), cat))
    {
        throw new ArgumentOutOfRangeException("cat");
    }
}

In C# 3 this can easily be done with an extension method:

// Can't constrain T to be an enum, unfortunately. This will have to do :)
public static void ThrowIfNotDefined<T>(this T value, string name) where T : struct
{
    if (!Enum.IsDefined(typeof(T), value))
    {
        throw new ArgumentOutOfRangeException(name);
    }
}

Usage:

public void Test(BookCategory cat)
{
    cat.ThrowIfNotDefined("cat");
}
Jon Skeet
It is a pain that generics don't allow "Enum"/"enum" as a constraint...
Marc Gravell
@Marc: Indeed. @Richard: Added.
Jon Skeet
Kent Boogaart
Wow, didn't realize C# enums were just ints really. Java enums really are so much better.
cletus
@cletus: Yes indeed. .NET enums aren't really OO at all :(
Jon Skeet
See http://blogs.msdn.com/brada/archive/2003/11/29/50903.aspx for reasons why you shouldn't use Enum.IsDefined.
Bert Huijben
@Bert: good link, thanks.
cletus
ThrowIfNotDefined is not an extension method. You missed a "this" before T value.
Allrameest
I edited the code to make it an extension method.
Steven Sudit
A: 

If you want to extend your enum with new values in future versions of your library shouldn't use the construct

if (Enum.IsDefined(typeof(BookCategory), cat))

As this only tests whether the type is valid according to the enum definition. Your code on the other hand was only tested for the currently allowed values and might fail for new values.

See This weblog article for more background information.

Bert Huijben
Well, if you (later) start using values for your enums that aren't defined in the enum, what's the point of having an enum in the first place?
Tor Haugen
The point is that you can later define new valid enum values (by recompiling your code).. But code compiled against your code wasn't tested with this new value... and Enum.IsDefined just returns true
Bert Huijben
+5  A: 

I think the comments above pretty much answered the question. Essentially, when I wrote this rule I was trying to convey a defensive coding practice of validating all inputs. Enums are a special case because many developers incorrectly assume that they are validated, when they are not. As a result, you will often see if statements or switch statements fail for an undefined enum value.

Just keep in mind that by default an enum is nothing more than a wrapper around an INT, and validate it just as if it were an int.

For a more detailed discussion of proper enum usage, you might check out blog posts by Brad Abrams and Krzysztof Cwalina or their excellent book "Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries"

Lance Hunt
Thanks for the reply Lance - any chance you could update your reply to include links to those blog posts?
Richard Ev