Enum Pattern
Enum Pattern
Introduction
A programmer often has to deal with a fixed number of cases in his program. For example, if your program is about emergency management, you may consider the following cases:
- Fire
- Injured
- Burglary
- Blue Screen of Death
- Run out of Beer
and your program may look like:
int emergency = 3;
switch (emergency)
{
case 1:
case 2:
case 3:
Console.WriteLine("Call 911");
break;
case 4:
Console.WriteLine("Sue Microsoft");
break;
case 5:
Console.WriteLine("Brew your own");
break;
}
Here, each emergency case is assigned a number. The problem with it is that the program becomes hard to read if the reader doesn't know what each number stands for. This is where enum
comes to the rescue:
enum Emergency
{
Fire = 1,
Injured,
Burglary,
Blue_Screen_of_Death,
Run_out_of_Beer
}
Emergency emergency = Emergency.Injured;
switch (emergency)
{
case Emergency.Fire:
case Emergency.Injured:
case Emergency.Burglary:
Console.WriteLine("Call 911");
break;
case Emergency.Blue_Screen_of_Death:
Console.WriteLine("Sue Microsoft");
break;
case Emergency.Run_out_of_Beer:
Console.WriteLine("Brew your own");
break;
}
So enum
is just a way of naming numbered cases. It makes your program more readable. In .NET, it's very easy to get the names of numbered cases as string
s:
Console.WriteLine(emergency.ToString());
//output: Injured
Console.WriteLine(string.Join(",", Enum.GetNames(typeof(Emergency))));
//output: Fire,Injured,Burglary,Blue_Screen_of_Death,Run_out_of_Beer
However, in principle, you should reject the temptation of using enum
names in the presentation layer, as the names are meant for the convenience of programmers, not for the users of your program. For example, "Blue_Screen_of_Death
" or even "BSoD
" is perfectly fine in your program, but when presented to the users, it's just not that user friendly.
What if you do want to associate a user-friendly string
to each enumerated case? Muaddubby in his article A Perfect C# String Enumerator was trying to do just that. He claims his string
enumerator has all the right behaviors of a real enum
. But he fails to address the fact that a real enum
is a value type constant so you can use it in a switch
statement and do flag bit operations with it when decorated with the [Flags]
attribute. So his string
enumerator is not that perfect after all. A better approach, as described in String Enumerations in C# by Matt Simner and Enum With String Values In C# by Stefan Sedich, is to leverage the existing enum
. They associate a string
to an enumerated case by way of a custom attribute, and use reflection to gain access to the string
via an extension method.
Enum Pattern
From previous efforts of devising a string
enumerator, it's evident that we need a general programming pattern of associating a string
, or any object for that matter, to an enumerated case. In fact, Matt Simner and Stefan Sedich in their articles have demonstrated a special case of the pattern. We just need to make it a little bit more generic so it can handle any object, not just a string
. Here is what is involved:
Step 1
We need to define a base class that represents the attribute of an enumerated case.
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class EnumAttr : Attribute
{
public EnumAttr()
{
}
}
Since the attribute can be any object, we don't want it to have any particular behaviors by implementing methods or properties, except that it must apply to an enum
field no more than once.
Step 2
We define the following extension method to gain access to the attribute object associated with an enum
field:
public static class EnumExtension
{
public static EnumAttr GetAttr(this Enum value)
{
Type type = value.GetType();
FieldInfo fieldInfo = type.GetField(value.ToString());
var atts = (EnumAttr[])fieldInfo.GetCustomAttributes(typeof(EnumAttr), false);
return atts.Length > 0 ? atts[0] : null;
}
}
Step 3
Define your own attribute class derived from EnumAttr
.
Step 4
Define your enum
and add an instance of your attribute class in Step 3 to each enum
field. Your enum
can be regular or flagged.
Step 5
Use your enum
as usual. Whenever you need to access the attribute data of an enum
field, just call the GetAttr()
extension method.
Examples
The other day, I was looking for something on Craig's List and wondered around to the personal classified ad section. To my surprise (well, not that surprised), I found that there were a lot more categories than just man-seeking-woman and woman-seeking-man. Here is a partial list:
public class SexInterestAttr : EnumAttr
{
public string Desc { get; set; }
}
public enum SexInterest
{
[SexInterestAttr(Desc = "woman seeking man")]
w4m,
[SexInterestAttr(Desc = "man seeking man")]
m4m,
[SexInterestAttr(Desc = "man seeking woman")]
m4w,
[SexInterestAttr(Desc = "woman seeking woman")]
w4w,
[SexInterestAttr(Desc = "couple seeking couple")]
mw4mw,
[SexInterestAttr(Desc = "couple seeking woman")]
mw4w,
[SexInterestAttr(Desc = "couple seeking man")]
mw4m,
[SexInterestAttr(Desc = "woman seeking couple")]
w4mw,
[SexInterestAttr(Desc = "man seeking couple")]
m4mw,
[SexInterestAttr(Desc = "woman seeking lesbian couple")]
w4ww,
[SexInterestAttr(Desc = "man seeking gay couple")]
m4mm,
[SexInterestAttr(Desc = "gay couple seeking man")]
mm4m,
[SexInterestAttr(Desc = "lesbian couple seeking woman")]
ww4w,
[SexInterestAttr(Desc = "lesbian couple seeking man")]
ww4m,
[SexInterestAttr(Desc = "gay couple seeking woman")]
mm4w,
[SexInterestAttr(Desc = "man seeking lesbian couple")]
m4ww,
[SexInterestAttr(Desc = "woman seeking gay couple")]
w4mm,
}
Here, our attribute class, SexInterestAttr
, contains only one property Desc
, a user-friendly string
to describe the sex interest. Notice that in our SexInterest
enum
, we can use the object initializer syntax to create an instance of SexInterestAttr
. The following code snippet shows how to use the enum
and its attribute:
var interests = (SexInterest[])Enum.GetValues(typeof(SexInterest));
Console.WriteLine(string.Join(System.Environment.NewLine,
interests.Select(i => string.Format("{0} - {1}", i.ToString(),
((SexInterestAttr)i.GetAttr()).Desc)).ToArray()));
Output:
w4m - woman seeking man
m4m - man seeking man
m4w - man seeking woman
w4w - woman seeking woman
mw4mw - couple seeking couple
mw4w - couple seeking woman
mw4m - couple seeking man
w4mw - woman seeking couple
m4mw - man seeking couple
w4ww - woman seeking lesbian couple
m4mm - man seeking gay couple
mm4m - gay couple seeking man
ww4w - lesbian couple seeking woman
ww4m - lesbian couple seeking man
mm4w - gay couple seeking woman
m4ww - man seeking lesbian couple
w4mm - woman seeking gay couple
Adding attribute to enum
doesn't alter its value type. We can still use it as usual such as in a switch
statement:
var interest = SexInterest.w4m;
switch (interest)
{
case SexInterest.m4m:
Console.WriteLine("you are a gay");
break;
case SexInterest.w4w:
Console.WriteLine("you are a lesbian");
break;
case SexInterest.m4w:
case SexInterest.w4m:
Console.WriteLine("you are a hetero");
break;
default:
Console.WriteLine("whatever. it's a free country");
break;
}
//ourput: you are a hetero
As a side point, one of the advantages of using extension method is that it seemingly becomes part of our enum
type and actually shows up in the IntelliSense:
The above example is quite simple. We only associate a string
to each enumerated case. Now let's take a look at a more complex example. Assume we have an enumeration of US presidents and each president can be described with the following PresidentAttr
class:
public class PresidentAttr : EnumAttr
{
public PresidentAttr(string name, string party, int yearTookOffice,
int yearBorn, int yearDied)
{
Name = name;
Party = party;
YearTookOffice = yearTookOffice;
YearBorn = yearBorn;
YearDied = yearDied;
}
public string Name { get; set; }
public string Party { get; set; }
public int YearTookOffice { get; set; }
public int YearBorn { get; set; }
public int YearDied { get; set; }
public bool IsAlive { get { return (YearDied <= 0); } }
public int AgeTookOffice { get { return YearTookOffice - YearBorn; } }
}
Our President enum
is defined as:
public enum President
{
[PresidentAttr("George Washington", "No Party", 1789, 1732, 1799)]
GeorgeWashington,
[PresidentAttr("John Adams", "Federalist", 1797, 1735, 1826)]
JohnAdams,
[PresidentAttr("Thomas Jefferson", "Democratic-Republican", 1801, 1743, 1826)]
ThomasJefferson,
...
[PresidentAttr("Bill Clinton", "Democratic", 1993, 1946, 0)]
BillClinton,
[PresidentAttr("George W. Bush", "Republican", 2001, 1946, 0)]
GeorgeWBush,
[PresidentAttr("Barack Obama", "Democratic", 2009, 1961, 0)]
BarackObama
}
Now we can have some funs with our President
enum:
var presidents = (President[])Enum.GetValues(typeof(President));
Console.WriteLine(string.Join(System.Environment.NewLine,
presidents.Select(p => (PresidentAttr)p.GetAttr()).
Select(a => string.Format("{0} ({1}-{2}), took office in {3}",
a.Name, a.YearBorn, (a.YearDied<=0)? "" : a.YearDied.ToString(),
a.YearTookOffice)).ToArray()));
Output:
George Washington (1732-1799), took office in 1789
John Adams (1735-1826), took office in 1797
Thomas Jefferson (1743-1826), took office in 1801
...
Bill Clinton (1946-), took office in 1993
George W. Bush (1946-), took office in 2001
Barack Obama (1961-), took office in 2009
Console.WriteLine("There are {0} Democratic presidents",
presidents.Count(p => ((PresidentAttr)p.GetAttr()).Party == "Democratic"));
Console.WriteLine("They are:");
Console.WriteLine(string.Join(System.Environment.NewLine,
Array.FindAll(presidents, p => ((PresidentAttr)p.GetAttr()).Party == "Democratic").
Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));
Output:
There are 16 Democratic presidents
They are:
Andrew Jackson
Martin Van Buren
James K. Polk
Franklin Pierce
James Buchanan
Andrew Johnson
Grover Cleveland
Grover Cleveland (2nd term)
Woodrow Wilson
Franklin D. Roosevelt
Harry S. Truman
John F. Kennedy
Lyndon B. Johnson
Jimmy Carter
Bill Clinton
Barack Obama
Console.WriteLine("Presidents still alive: {0}",
string.Join(",", Array.FindAll(presidents, p =>
((PresidentAttr)p.GetAttr()).IsAlive).Select(p =>
((PresidentAttr)p.GetAttr()).Name).ToArray()));
Output:
Presidents still alive:
Jimmy Carter,George H. W. Bush,Bill Clinton,George W. Bush,Barack Obama
int maxAge = presidents.ToList().Max(p =>((PresidentAttr)p.GetAttr()).AgeTookOffice);
var maxPAttr = (PresidentAttr)Array.Find(presidents, p =>
((PresidentAttr)p.GetAttr()).AgeTookOffice == maxAge).GetAttr();
Console.WriteLine("The oldest president was {0}, a {1}, who took office at age {2}.",
maxPAttr.Name, maxPAttr.Party, maxPAttr.AgeTookOffice);
Output:
The oldest president was Ronald Reagan, a Republican, who took office at age 70.
Our last example is going to demonstrate President
enum
with [Flags]
attribute, or PresidentFlagged
. Here is its definition:
[Flags]
public enum PresidentFlagged : long
{
[PresidentAttr("George Washington", "No Party", 1789, 1732, 1799)]
GeorgeWashington = 0x1,
[PresidentAttr("John Adams", "Federalist", 1797, 1735, 1826)]
JohnAdams = 0x2,
[PresidentAttr("Thomas Jefferson", "Democratic-Republican", 1801, 1743, 1826)]
ThomasJefferson = 0x4,
...
[PresidentAttr("Bill Clinton", "Democratic", 1993, 1946, 0)]
BillClinton = 0x20000000000,
[PresidentAttr("George W. Bush", "Republican", 2001, 1946, 0)]
GeorgeWBush = 0x40000000000,
[PresidentAttr("Barack Obama", "Democratic", 2009, 1961, 0)]
BarackObama = 0x80000000000,
_MostInfluential = AbrahamLincoln | FranklinDRoosevelt | GeorgeWashington
| ThomasJefferson | AndrewJackson | TheodoreRoosevelt,
_DiedInOffice = WilliamHenryHarrison | ZacharyTaylor | AbrahamLincoln | JamesAGarfield
| WilliamMcKinley | WarrenGHarding | FranklinDRoosevelt | JohnFKennedy,
_Assassinated = AbrahamLincoln | JamesAGarfield | WilliamMcKinley | JohnFKennedy,
}
Notice that we need to assign an integer number to each enum
field in the order of 2^n in order for the flag bit operation to work. Also, we predefined three combined values, _MostInfluential
, _DiedInOffice
and _Assassinated
. I prefer to prefix the combined values with an underscore "_
" so they can be differentiated from single values and are put on top of the list of all possible values when displayed in the IntelliSense (see image below).
In the example, we also need the following helper method to parse a combined enum
value into individual single enum
values:
private static PresidentFlagged[] ParseValues(PresidentFlagged combinedValue)
{
List<PresidentFlagged> values = new List<PresidentFlagged>();
PresidentFlagged[] allvalues = (PresidentFlagged[])Enum.
GetValues(typeof(PresidentFlagged));
return Array.FindAll(allvalues, v =>
((combinedValue & v) == v) && !v.ToString().StartsWith("_"));
}
Notice when finding individual single values of a combined enum
value, we exclude the predefined combined values that are identified by the "_
" prefix. Now the fun part:
PresidentFlagged jfk = PresidentFlagged.JohnFKennedy;
var jfkAttr = (PresidentAttr)jfk.GetAttr();
Console.WriteLine("Was {0} died in office? {1}", jfkAttr.Name,
((PresidentFlagged._DiedInOffice & jfk) == jfk));
Console.WriteLine("Was he Assassinated? {0}",
((PresidentFlagged._Assassinated & jfk) == jfk));
Output:
Was John F. Kennedy died in office? True
Was he Assassinated? True
var Influentials = ParseValues(PresidentFlagged._MostInfluential);
Console.WriteLine("Most influential presidents:");
Console.WriteLine(string.Join(System.Environment.NewLine,
Influentials.Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));
Output:
Most influential presidents:
George Washington
Thomas Jefferson
Andrew Jackson
Abraham Lincoln
Theodore Roosevelt
Franklin D. Roosevelt
var myFavorites = ParseValues(PresidentFlagged.AbrahamLincoln
| PresidentFlagged.FranklinDRoosevelt
| PresidentFlagged.BillClinton | PresidentFlagged.BarackObama);
Console.WriteLine("My favorite presidents:");
Console.WriteLine(string.Join(System.Environment.NewLine,
myFavorites.Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));
Console.WriteLine("Are they all Democratic? {0}",
myFavorites.All(p => ((PresidentAttr)p.GetAttr()).Party == "Democratic"));
Output:
My favorite presidents:
Abraham Lincoln
Franklin D. Roosevelt
Bill Clinton
Barack Obama
Are they all Democratic? False
Conclusions
An enum
pattern, in which each enumerated case is associated to an instance of a generic attribute class, is identified and implemented. It is particularly useful in situations where there is a finite number of cases and each case is characterized by a set of constants.
You can download the C# project from the link at the top of this post.