Introduction
It was simply that I wanted to protect against erroneous input to a method that took an enum
as a parameter, only I wasn't sure how to go about it. So I stumbled upon Enum.IsDefined
and then spent quite some time trying to work out how it behaved under different circumstances, i.e., what response was given to different inputs. On reflection, neither using an enum
in the particular scenario I had nor using IsDefined
are that smart, using the Strategy Pattern[^] instead would probably be a better idea.
So whilst there are better ways to tackle most problems such that you can avoid using enum
s and switches (and IsDefined
) altogether, if you ever do need to use Enum.IsDefined
and want some info on its behaviour, then this may help as a reference.
Summary
The rest of the article explains how I came to this conclusion, stop after the table below if all you want is the summary.
Be aware that this table only applies if you create the object and send it to Enum.IsDefined
from within the same method or at least you have a way of not needing to pass the object to the method that contains the Enum.IsDefined
call.
As soon as you start passing the object
which may (or may not) be an enum
of the relevant type to other methods (which is essentially the scenario I had and within which I wanted to protect against bad input), you very quickly get into a mess with passing things as generic object
s rather than specific enum
objects followed by casting exceptions (or you can just go directly to casting exceptions if you prefer). This almost answers the original question in fact.
Summary of the Behaviour exhibited by Enum.IsDefined methodInput | Example Value | Returns | Exception |
---|
Member of enumeration | TestEnum.Bees | True | - |
---|
String representation of member of enumeration | "Bees " | True | - |
---|
Integer within range of the enumeration | 3 | True | - |
---|
Integer variable within range of the enumeration | int num = 3 | True | - |
---|
String representation of integer within range of the enumeration | "three " | False | - |
---|
Integer outside the range of the enumeration | 7 | False | - |
---|
Integer variable outside range of the enumeration | int num = 7 | False | - |
---|
String representation of integer outside range of the enumeration | "seven " | False | - |
---|
Member of another enumeration within same range as test enumeration | OtherEnum.Bandersnatch | - | ArgumentException |
---|
String representation of member of another enumeration within same range as test enumeration | "Bandersnatch " | False | - |
---|
Member of another enumeration outside range of test enumeration | OtherEnum.Manxome | - | ArgumentException |
---|
String representation of member of another enumeration outside range of test enumeration | "Manxome " | False | - |
---|
Object, not part of an enumeration | Color.Blue | - | InvalidOperationException |
---|
Null | int? num = null | - | ArgumentNullException |
---|
Two scenarios, (both within the .NET environment):
First Scenario
In the first one, we do everything in one method or have some way of passing the object
under investigation without needing to cast. The table above represents this scenario exactly and there are essentially no constraints in terms of what objects you can throw at IsDefined
since its signature is (Type enumType, Object value)
.
Second Scenario
In the second, we pass an object
that has as its type
the type
of the relevant enum
to some method (e.g. after refactoring for the above example). Here, it's somewhat simpler:
- An
enum
is a value
type so is not nullable - Any attempt to pass anything other than the relevant
enum
type to the method that does the test work simply won't compile
So it would appear that in this scenario, the only thing we need to protect against is a change to the enum
which is not reflected in the switch
statement, and this isn't a problem that IsDefined
will help us with.
Errors caused by casting another type
to the relevant enum
type are outside the scope of what we are trying to protect against and would need to be handled by the calling code.
What would happen if this was a library and an external program tried to access the method with bad data? (I have no idea.)
Contents
- Background
- Trapping Exceptions
- Syntax
- Returns
- Exceptions
- ArgumentNullException
- ArgumentException
- InvalidOperationException
- Code to Discover Behaviour
- Code to Handle Bad Input
- Points of Interest
- History
Background
Example method, the intention was to allow the user of the class that contains this method to specify one of various different ways of converting a color to grey.
ToGrey(ToGreyMethod toGreyMethod)
{
Switch (toGreyMethod) {
case ToGreyMethod.Decompose:
this.Decompose;
break;
case ToGreyMethod.Desaturate:
this.Desaturate;
break;
case ToGreyMethod.Brightness:
this.Brightness;
break;
default:
throw new NotImplementedException("We are dreadfully sorry,
but we really don't understand what has caused this error.
Please do let us know about our intolerable incompetence.", ex);
break;
}
}
Do we need to trap any exceptions
that could be caused by any errors emanating from the supplied toGreyMethod
?
Probably we do, yes: Exceptions for Enum Method Parameters[^]
How Can We Do That?
Well, before we try and use the supplied method parameter, we can test to see if it is indeed a member of the enumeration and the enum
class provides a method to do so, Enum.IsDefined
.
And that's when my problems[^] really started. The IsDefined
method is defined thus:
Syntax
public static bool IsDefined(
Type enumType,
Object value
)
Returns
true
if a constant in enumType
has a value equal to value
; otherwise, false
.
Exceptions
ArgumentNullException
enumType
or value
is null
.
ArgumentException
enumType
is not an Enum
.
-or-
The type of value
is an enumeration, but it is not an enumeration of type enumType
.
-or-
The type of value
is not an underlying type of enumType
.
InvalidOperationException
value
is not type SByte
, Int16
, Int32
, Int64
, Byte
, UInt16
, UInt32
, or UInt64
, or String
.
To which my first question was: How can that ever return false
? It seemed to be that it was either going to return true
or throw an exception
.
It looked like the exceptions
were being used to provide information with a bit more depth than returning a simple false
. That doesn't feel particularly good, so either I'm using the method in a way that it wasn't intended to be used or it's not the greatest piece of design ever to escape into production.
Trying to work this out logically on paper and my head wasn't going to work, so time for a scratch project to test what happens when you throw various inputs at the IsDefined
method.
Test Code
There are two enumerations, TestEnum
with 6 members and OtherEnum
with 7 members. TestEnum
is the one we will test the objects against for membership.
The static void EnumIsDefined()
method sets up an array
of objects
, each of which we will test in the IsDefined
method to see what happens, what errors are thrown, etc. We have populated the array with 7 objects
to determine the behaviour of IsDefined
in a variety of circumstances.
TestEnum.Bees
: This is a member of TestEnum
and so should return true
with no exceptions
string variable = "Bees"
: Can the method manage the string
representation of enumeration member? 3 (integer)
: There are more than three members of the enumeration so this should also return true
with no exceptions
integer variable = 3
: Is there a difference between the number 3
and a variable with a value of 3
(I hope not!)? string variable = "three"
: And what about a string
representation of an integer
? 7 (integer)
: There are only 6 members of the enumeration, I wasn't clear whether this would throw an exception
or return false
integer variable = 7
: Is there a difference between an out of range number and a variable with the same value? string variable = "seven"
: And what about a string
representation of an out of range integer
? OtherEnum.Bandersnatch
: This is a member of another enumeration but its position in that enumeration (2nd) is within the range of members of TestEnum
(6) string variable = "Bandersnatch"
: Is there a difference between a member of a different enumeration and its string
representation, positional value in range of test enumeration? OtherEnum.Manxome
: This is a member of another enumeration and its position in that enumeration (7th) is outside the range of members of TestEnum
(6) string variable = "Manxome"
: Is there a difference between a member of a different enumeration and it's string
representation, positional value out of range of test enumeration? Color.Blue
: Just a random object I picked to see what happens Null
: Specific case
You can see that we've extracted out a method to provide greater detail concerning any exception that is thrown but, and this will become important later, the array of objects is tested against IsDefined
within the same method that we enumerated over the array, i.e., we are not passing the enum
from one method to another.
using System;
using System.Drawing;
namespace Cs_Scratch_Console
{
class Program
{
static void Main(string[] args)
{
EnumIsDefined();
}
static void EnumIsDefined()
{
Console.WriteLine("Press any key to test the behaviour of Enum.IsDefined...");
Console.WriteLine();
Console.ReadKey(true);
object value;
int? num = null;
string printValue;
object[] values = new object[] { TestEnum.Bees, 3, 7, OtherEnum.Bandersnatch,
OtherEnum.Manxome, Color.FromName("Blue"), num };
for (int counter = 0; counter <= values.Length - 1; counter++ )
{
value = values[counter];
if (value == null)
{
printValue = "Null";
}
else
{
printValue = value.ToString();
}
Console.WriteLine("Current array counter is: {0}", counter);
Console.WriteLine("Current test value is: {0}", printValue);
if (value != null)
{
Console.WriteLine("It has a type of: {0}", value.GetType());
}
else
{
Console.WriteLine("It has a type of: No Type available, object is null");
}
Console.WriteLine("Press any key to test whether {0} throws an exception
when we try 'Enum.IsDefined(typeof(TestEnum), {0})'...", printValue);
Console.ReadKey(true);
try
{
if (Enum.IsDefined(typeof(TestEnum), value))
{
Console.WriteLine("{0} is recognised as a defined enumeration
constant within TestEnum", printValue);
Console.ReadKey(true);
Console.WriteLine();
Console.WriteLine("Press any key for the next test...");
Console.WriteLine();
Console.ReadKey(true);
}
else
{
Console.WriteLine("{0} is NOT recognised as a defined enumeration
constant within TestEnum", printValue);
Console.ReadKey(true);
Console.WriteLine();
Console.WriteLine("Press any key for the next test...");
Console.WriteLine();
Console.ReadKey(true);
}
}
catch (Exception ex)
{
Console.WriteLine("An error was thrown. Press any key to find out more
about which error...", printValue);
Console.ReadKey(true);
EnumIsDefinedConsoleWriter(ex);
Console.WriteLine();
Console.WriteLine("Press any key for the next test...");
Console.WriteLine();
Console.ReadKey(true);
}
}
Console.WriteLine("Press any key to close the demo...");
Console.ReadKey(true);
}
static void EnumIsDefinedConsoleWriter(Exception ex)
{
Console.WriteLine(ex.GetType().ToString());
switch (ex.GetType().ToString())
{
case "System.ArgumentNullException":
Console.WriteLine("The supplied method parameter, which should be a member
of the enumeration TestEnum, is Null (Nothing in Visual Basic)");
break;
case "System.ArgumentException":
Console.WriteLine("The supplied method parameter is a member of an enumeration
but it is not of the type TestEnum");
break;
case "System.InvalidOperationException":
Console.WriteLine("The value parameter provided is not a number of type SByte,
Int16, Int32, Int64, Byte, UInt16, UInt32, or UInt64, or a String representation
of the name of an enumeration member.");
break;
default:
Console.WriteLine("We are dreadfully sorry, but we really don't understand
what has caused this error. Please do let us know about
our intolerable incompetence.");
break;
}
}
public enum TestEnum
{
Sieve,
Timballo,
Owl,
Bees,
Pig,
Stilton
}
public enum OtherEnum
{
JubJub,
Bandersnatch,
Vorpal,
Tumtum,
Tulgey,
Beamish,
Manxome
}
}
}
As noted above, we loop through the array with each array member being tested against the enumeration with the Enum.IsDefined
method. A positive match gets one message and a negative result another message. We catch and report the exceptions individually, this way we get to know exactly what type of 'error' in the parameter passed to the IsDefined
method causes which type of exception.
Summary of the Behaviour exhibited by Enum.IsDefined methodInput | Example Value | Returns | Exception |
---|
Member of enumeration | TestEnum.Bees | True | - |
---|
String representation of member of enumeration | "Bees " | True | - |
---|
Integer within range of the enumeration | 3 | True | - |
---|
Integer variable within range of the enumeration | int num = 3 | True | - |
---|
String representation of integer within range of the enumeration | "three " | False | - |
---|
Integer outside the range of the enumeration | 7 | False | - |
---|
Integer variable outside range of the enumeration | int num = 7 | False | - |
---|
String representation of integer outside range of the enumeration | "seven " | False | - |
---|
Member of another enumeration within same range as test enumeration | OtherEnum.Bandersnatch | - | ArgumentException |
---|
String representation of member of another enumeration within same range as test enumeration | "Bandersnatch " | False | - |
---|
Member of another enumeration outside range of test enumeration | OtherEnum.Manxome | - | ArgumentException |
---|
String representation of member of another enumeration outside range of test enumeration | "Manxome " | False | - |
---|
Object, not part of an enumeration | Color.Blue | - | InvalidOperationException |
---|
Null | int? num = null | - | ArgumentNullException |
---|
With that, we can probably write a sensible method that captures the various possible results and responds appropriately and informatively in each case.
Potential Code
So here goes, we have three cases, positive match, no match and an exception (three types of exception).
Warning! There are better ways to do this type of stuff, it's a demo to work out how Enum.IsDefined
functions.
static void ExampleEnumMethod(TestEnum method)
{
try
{
if(Enum.IsDefined(typeof(TestEnum), method))
{
switch (method) {
case TestEnum.Bees:
Console.WriteLine("Bees");
break;
case TestEnum.Owl:
Console.WriteLine("Owl");
break;
case TestEnum.Pig:
Console.WriteLine("Pig");
break;
case TestEnum.Sieve:
Console.WriteLine("Sieve");
break;
case TestEnum.Stilton:
Console.WriteLine("Stilton");
break;
case TestEnum.Timballo:
Console.WriteLine("Timballo");
break;
default:
Console.WriteLine("It would appear that the TestEnum enumeration has been changed
or extended but that this method has not been updated to match the new definition,
we are dreadfully sorry about that.");
break;
}
else
{
Console.WriteLine("The supplied method parameter is not an enumerable type
but we were able to test it against TestEnum to see if we could make a match,
(for a example it was a string or an integer), unfortunately there was no member
of TestEnum which appeared to match the supplied object");
}
}
catch (Exception ex)
{
switch (ex.GetType().ToString())
{
case "System.ArgumentNullException":
Console.WriteLine("The supplied method parameter,
which should be a member of the enumeration TestEnum,
is Null (Nothing in Visual Basic)");
break;
case "System.ArgumentException":
Console.WriteLine("The supplied method parameter is a
member of an enumeration but it is not of the type TestEnum");
break;
case "System.InvalidOperationException":
Console.WriteLine("The value parameter provided is not a
number of type SByte, Int16, Int32, Int64, Byte, UInt16, UInt32,
or UInt64, or a String representation of the name of an enumeration member.");
break;
default:
Console.WriteLine("We are dreadfully sorry,
but we really don't understand what has caused this error.
Please do let us know about our intolerable incompetence.");
break;
}
}
}
Only it's not that simple. You can't compile this when the method that sends the object to test to the ExampleEnumMethod
method is an array of object
, i.e., since the signature of the method is (TestEnum method)
and the array is of object
s the compiler complains, not unreasonably. So I changed the calling line from ExampleEnumMethod(value);
to ExampleEnumMethod((TestEnum)value);
.
Only you can't cast an obect
to an enum
, the compiler doesn't complain but at run-time, you get invalid cast exceptions. You can't even pass the types that can be explicitly tested against the enum
by the EnumIsDefined
method, SByte
, Int16
, Int32
, Int64
, Byte
, UInt16
, UInt32
, or UInt64
, or String
, i.e., a string
representation of the name of an enumeration member or a value of the underlying type of the enum
(or one that can be implicitly cast to it).
I am therefore guessing that as long as you are in the .NET environment, then:
- You simply can't compile code that tries to pass anything other than a variable of the relevant
enum
type to a method with that enum
in the method signature - Since an
enum
is not nullable, we don't need to protect against Null
values - You can't pass an array of the relevant
enum
type (compiler complains again) so we don't need to handle that case - If we wanted to offer the ability to pass integers or
string
s, we would need to overload, otherwise the user of the method will need to handle this themselves
If of course, we want to offer this as part of some class in a library, then we might want to behave differently, I have no (and probably won't in the near future) worry about this.
It is still a good idea to trap some errors, such as the default
case for when the enum
is changed but the switch
is not updated to reflect it, however the run time errors have to be trapped from the code that calls the method.
Points of Interest
Side note: Consider these three articles for better ways of working with enumerations and particularly if you find yourself using switch
es.
Personally, I'll probably replace it with the Strategy Pattern.
History
- October 2013: Version 1.0
- October 2013: Version 1.1: Added source code download
- August 2017: Version 1.2: Added link to C# Replacing switch(enum) flow control with Reflection article