1. Overview
An Enumeration(enum) in C# is a set of value type variables defined by one or more constants whose default value is of type integer. In C#, enums cannot have behavior, that is, methods that perform any sort of processing or calculation, neither can we expose getters as we would in a regular class or using enums in some other languages, at least not out of the box, more on that later on the Workaround section in this article.
We use enums to enumerate a finite number of unique values represented by related constants. They are unique values because you cannot have two enums with the same identifier and they must be bound to a value whether we explicitly bind or let the compiler do it implicitly.
Note: for our example we will be using .NET Framework 4.8.
2. How to Declare an Enum in C#
Enums in C# may be declared in a separate file, but, many times, enums are placed in the class file that makes more sense. Keep in mind that by putting enums in a class file you are tightly coupling them and if, in the future you were to need this very enum
for a different class or namespace, refactoring would be on the way.
Let’s declare our first enum
in a separate file called DaysOfWeek.cs:
namespace Enumeration { internal enum DaysOfWeek // An Integer by default : int { Sunday, // 0 by default Monday, // 1 by default Tuesday, // 2 by default Wednesday,// 3 by default Thursday, // 4 by default Friday, // 5 by default Saturday // 6 by default } }
In C#, all enums must be numeric values, as mentioned before, of the following types: byte
, sbyte
, short
, ushort
, int
, uint
, long
, or ulong
. Any other type will result in compilation error. Note the s on sbyte
stand for signed and u on ushort
, uint
and ulong
stands for unsigned. Let’s change our enum to byte.
internal enum DaysOfWeek : byte // now your enum is of type byte { Sunday, // 0 by default Monday, // 1 by default Tuesday, // 2 by default Wednesday,// 3 by default Thursday, // 4 by default Friday, // 5 by default Saturday, // 6 by default }
2.1 Looping Through Enums
Now, in our Main method, let’s loop through our enum
and print its value. Note that although enums are numeric values, its ToString()
method will print a string representation equals to its identifier. If you define it all upper case, it will print like so, if you define it all lower case, it will print like so. Conventionally, we use Pascal Case for identifiers in C#.
using System; namespace Enumeration { internal class Program { static void Main(string[] args) { // We must call the static method GetValues in the Enum class to iterate over the enums. foreach (var day in Enum.GetValues(typeof(DaysOfWeek))) { Console.WriteLine("{0} Integer value is {1}", day, (int)day); // Explicit cast to int } Console.ReadKey(true); // Waits for the user to press any key, true not to show the key. } } }
Output:
Sunday Integer value is 0 Monday Integer value is 1 Tuesday Integer value is 2 Wednesday Integer value is 3 Thursday Integer value is 4 Friday Integer value is 5 Saturday Integer value is 6
As you can see, the ToString()
method which was called implicitly returned the literal representation of each enum
identifier and to obtain its value, we needed to explicitly cast them into integers. However, there may be scenarios where all you need is the string representation or just its numerical value.
Should you need the string representation of each enum:
Console.WriteLine("Days of the Week as string:"); // UNCOMMENT TO THROW AN InvalidCastException. //foreach (string day in Enum.GetValues(typeof(DaysOfWeek))) //{ // Console.WriteLine(day); //Throws InvalidCastException //} foreach (string day in Enum.GetNames(typeof(DaysOfWeek))) { Console.WriteLine(day); }
Output:
Days of the Week as string: Sunday Monday Tuesday Wednesday Thursday Friday Saturday
As you saw, instead of calling Enum.GetValues()
, we called Enum.GetNames()
which return an array of strings.
Now let’s say you want just the numerical value from an enum
, easy-peasy.
Console.WriteLine("Days of the Week as byte:"); // It is possible to cast directly to whatever type the enum was specified. foreach (byte day in Enum.GetValues(typeof(DaysOfWeek))) // Works like a charm { Console.WriteLine(day); }
It is possible to loop through enums using a while
or a for
loop and though it may be more error prone, it is still a possibility.
public static void PrintEnumUsingForLoop() { Console.WriteLine("Days of the Week with a regular loop:"); var day = DaysOfWeek.Sunday; // First element in our enum whose value is 0 (zero). while ((byte)day < Enum.GetValues(typeof(DaysOfWeek)).Length) { Console.WriteLine(day++); // You can treat enums in C# as you would with any numeric value. } }
Note that it is possible to increment an enum
like you would with any other numeric value. Actually, any arithmetical operation is possible, though not recommended, since enums in C# are numeric values with sugar-coating to make it more readable and less error-prone.
Output:
Days of the Week with a regular loop: Sunday Monday Tuesday Wednesday Thursday Friday Saturday
3. C# Enum class static methods
Because of the way enums are implemented in C#, we cannot have behavior, or methods, in order to manipulate data on our application using enums, we need to use the static methods in the Enum abstract class.
3.1 Array GetValues(Type enumType)
This method returns an array of objects containing all the enum
constants.
- Parameter: enumType – an Enumeration type.
- Returns: an array containing the constants in enumType specified.
- Throws:
ArgumentNullException
if enumType isnull
. - Throws:
ArgumentException
if enumType is not anEnum
. - Throws:
InvalidOperationException
if the method is invoked by reflection in a reflection-only context, -or- enumType is a type from an assembly loaded in a reflection-only context.
Reflection-only context happens when you can load and read the code, but not execute it, that is, objects cannot be created, and dependencies are not automatically loaded.
Note that the abstract class Array is the base type for all array types in C#.
3.2 string[] GetNames(Type enumType)
Retrieves a string array of the names of the identifiers from the specified enumeration.
- Parameter: enumType – an Enumeration type.
- Returns: a string array of the identifiers of the constants in enumType.
- Throws:
ArgumentNullException
if enumType isnull
. - Throws:
ArgumentException
if enumType is not anEnum
.
3.3 bool TryParse<TEnum>(string value, out TEnum result)
Converts the string representation of the name or a numeric value of one or more enumerated constants to an equivalent enumerated object. The return value indicates whether the conversion succeeded.
- Parameter: value – the case-sensitive string representation of the enumeration name (identifier) or underlying value to convert.
- Parameter: result – if parse succeeds, the result will contain an object of the specified type, if it fails, it will contain the default value whether it may be a member of Enum type or not.
- Parameter Type: TEnum – the enumeration type to which to convert the value.
- Returns: true if the value parameter parses successfully; otherwise, false.
- Throws:
ArgumentException
if TEnum is not anEnum
type.
Note that in C# em use the out
keyword for a parameter when we want to pass arguments as reference types, and we want our method to return multiple values. In Try Parse methods, the out
parameter must NOT be initialized, and it functions as a success variable, that is, if the conversion succeeds, this variable will hold the converted value, otherwise the default value for the underlying type, for numeric value it will be 0 (zero). Bear in mind that out
variables will be initialized regardless.
The try parse method accepts either the enum
identifier or its numeric value as a parameter, both of which need to be in the string
format. Let’s add a method that takes a string
, tries to convert it into an enum and finally prints it to the console.
private static void StringToEnum(string day) { //Console.WriteLine(favoriteDay); // Not possible, enum has not been initialized. DaysOfWeek favoriteDay; // Declare a variable of type DaysOfWeek that CANNOT be initialized. if (Enum.TryParse(day, out favoriteDay)) { Console.WriteLine("Your favorite Day of the Week is {0}", favoriteDay); } else { // Even if the conversion fails, the enum will still be set to its default value, Sunday. Console.WriteLine("There is no such a day as {0}", day); // Even if the conversion fails, it will still print Sunday since it is associated with the default value. Console.WriteLine("I failed Star Wars and now I am {0}", favoriteDay); } }
Now for our Main method:
StringToEnum("Monday"); // Try different weekdays StringToEnum("6"); // Try different numbers StringToEnum("Star Wars Day"); // =^_^=
Output:
Your favorite Day of the Week is Monday Your favorite Day of the Week is Saturday There is no such day as Star Wars Day I failed Star Wars and now I am Sunday
To circumvent this issue, we will modify our enum
to start at one, which makes more sense since Sunday is the first day of the week. Keep in mind that if you have an enum
associated with the value zero, even if the conversion fails, it would still be a valid enum
and it would output whatever identifier is associated with the default number zero, as shown in the previous example.
namespace Enumeration { internal enum DaysOfWeek : byte // now your enum is of type byte { Unknown = 0, Sunday = 1, Monday = 2, Tuesday = 3, Wednesday = 4, Thursday = 5, Friday = 6, Saturday = 7 } }
Output:
Your favorite Day of the Week is Monday Your favorite Day of the Week is Friday There is no such day as Star Wars Day I failed Star Wars and now I am Unknown
Note that if there isn’t any constant associated with 0 (zero). it will print only the value 0 (zero).
3.4 TryParse<TEnum>(string value, bool ignoreCase, out TEnum result)
It works just like the previous method, except it will ignore the string case, that is, it won’t matter whether you write it all capitalized or not. The TryParse
method with two arguments also calls this one passing false
for the ignoreCase
parameter.
- Parameter: ignoreCase –
true
to ignore case;false
to consider case.
private static void StringToEnumIgnoreCase(string day) { // From C# 7.0 on it is possible to declare the out variable within the method call like so. if (Enum.TryParse(day, true, out DaysOfWeek favoriteDay)) { Console.WriteLine("Your favorite Day of the Week is {0}", favoriteDay); } else { Console.WriteLine("There is no such day as {0}", day); } }
In our Main method, let’s call our method passing some string literals capitalized differently.
StringToEnumIgnoreCase("SUNDAY"); StringToEnumIgnoreCase("wednesday"); StringToEnumIgnoreCase("FRiDay");
Output:
Your favorite Day of the Week is Sunday Your favorite Day of the Week is Wednesday Your favorite Day of the Week is Friday
3.5 object Parse(Type enumType, string value)
Converts the string
representation of the name or numeric value of one or more enumerated constants to an equivalent enumerated object.
- Parameter: enumType – an Enumeration type.
- Parameter: value – the case-sensitive string representation of the enumeration name (identifier) or underlying value to convert.
- Returns: an object of type enumType containing the
enum
value, if successful. - Throws:
ArgumentNullException
if enumType is null. - Throws:
ArgumentException
if neither the identifier nor the value are in theenum
. - Throws:
OverflowException
if value is outside the range of the underlying type of enumType.
private static void StringToObject(string enumeration) { // If parse fails it will throw an Exception. object day = Enum.Parse(typeof(DaysOfWeek), enumeration); Console.WriteLine(day); }
Main method call:
StringToObject("Thursday"); StringToObject("3"); //StringToObject("Weekend"); // UNCOMMENT to get an ArgumentException. //StringToObject("-128"); // UNCOMMENT to get an OverflowException
Output:
Thursday Tuesday
3.6 object Parse(Type enumType, string value, bool ignoreCase)
It works just like the previous method, except it will ignore the string case, that is, it won’t matter whatever capitalization system you use.
- Parameter: ignoreCase –
true
to ignore case;false
to consider case.
private static void StringToObjectIgnoreCase(string enumeration) { // If parse fails it will throw an Exception. object day = Enum.Parse(typeof(DaysOfWeek), enumeration, true); Console.WriteLine(day); }
Main method call:
StringToObjectIgnoreCase("WEDNESDAY"); StringToObjectIgnoreCase("sunday"); StringToObjectIgnoreCase("SaTuRDay");
Output:
Wednesday Sunday Saturday
3.7 Type GetUnderlyingType(Type enumType)
Returns the underlying type of the specified enumeration.
- Parameter: enumType – an Enumeration type.
- Returns: the underlying type of enumType.
- Throws:
ArgumentNullException
if enumType is null. - Throws:
ArgumentException
if enumType is not anenum
.
private static void GetUnderlyingType() { Type underlyingType = Enum.GetUnderlyingType(typeof(DaysOfWeek)); Console.WriteLine("My type is: {0}", underlyingType.Name); Console.WriteLine("Am I a value type: {0}", underlyingType.IsValueType); }
Output:
My type is: Byte Am I a value type: True
3.8 string GetName(Type enumType, object value)
Retrieves the name of the constant in the specified enumeration that has the specified value
- Parameter: enumType – an Enumeration type.
- Parameter: value – the value of an enumerated constant in terms of its underlying type.
- Returns: a string containing the name of the enumerated constant,
null
if not found. - Throws:
ArgumentNullException
if enumType isnull
. - Throws:
ArgumentException
if the value is not in theenum
.
private static void GetName(object value) { string day = Enum.GetName(typeof(DaysOfWeek), value); if (day != null) { Console.WriteLine("It is a valid enum: {0}", day); } else { Console.WriteLine("Not a valid enum!"); } }
Main method call:
GetName(6); // A byte value, you may try different numbers GetName(8); // null, there is no enum of value 8 whose underlying type is byte.
Output:
It is a valid enum: Friday Not a valid enum!
3.9 object ToObject(Type enumType, object value)
Converts the specified object with an integer value to an enumeration member.
- Parameter: enumType – an Enumeration type to return.
- Parameter: value – the value to convert to an enumeration member.
- Returns: an enumeration object, or a numeric value if not found.
- Throws:
ArgumentNullException
if enumType isnull
. - Throws:
ArgumentException
if neither the identifier nor the value are in theenum
.
private static void ToObject(object value) { object day = Enum.ToObject(typeof(DaysOfWeek), value); Console.WriteLine("It is a valid enum: {0}", day); }
Main method call:
ToObject(1); // A byte value, you may try different numbers
Output:
It is a valid enum: Sunday
3.10 bool IsDefined(Type enumType, object value)
Returns a Boolean telling whether a given integral value, or its name as a string, exists in a specified enumeration.
- Parameter: enumType – an Enumeration type to return.
- Parameter: value – the value or name of a constant in enumType.
- Returns:
true
if a constant in enumType has a value equal to value; otherwise,false
. - Throws:
ArgumentNullException
if enumType isnull
. - Throws:
ArgumentException
if neither the identifier nor the value are in theenum
. - Throws:
InvalidOperationException
if value is not of type SByte, Int16, Int32, Int64, Byte, UInt16, UInt32, UInt64, or String.
private static void IsDefinied(object value) { if (Enum.IsDefined(typeof(DaysOfWeek), value)) { Console.WriteLine(Enum.Parse(typeof(DaysOfWeek), value.ToString())); } }
Main method call:
IsDefinied("Thursday"); IsDefinied((byte)7); // If not cast, would result in exception
Output:
Thursday Saturday
3.11 string Format(Type enumType, object value, string format)
Converts the specified value of a specified enumerated type to its equivalent string
representation according to the specified format.
- Parameter: enumType – the enumeration type of the value to convert.
- Parameter: value – the value to convert.
- Parameter: format – the output format to use.
- Returns: a string representation of value.
- Throws: ArgumentNullException if the enumType, value, or format parameter is
null
. - Throws:
ArgumentException
if the enumType parameter is not an enum type, or the value is from an enumeration that differs in type from enumType, or the type of value is not an underlying type of enumType. - Throws:
FormatException
if the format parameter contains an invalid value. - Throws:
InvalidOperationException
format equals “X”, but the enumeration type is unknown.
private static void Format(DaysOfWeek value) { // If value is equal to a named enumerated constant, the name of that constant is returned; // otherwise, the decimal equivalent of value is returned. Console.WriteLine(Enum.Format(typeof(DaysOfWeek), value, "G")); // Also "g" // Represents value in hexadecimal format without a leading "0x". Console.WriteLine(Enum.Format(typeof(DaysOfWeek), value, "X")); // Also "x" // Represents value in decimal form. Console.WriteLine(Enum.Format(typeof(DaysOfWeek), value, "D")); // Also "d" // Behaves identically to "G" or "g", // Except that the FlagsAttribute is not required to be present on the Enum declaration. Console.WriteLine(Enum.Format(typeof(DaysOfWeek), value, "F")); // Also "f" }
Main method call:
Format(DaysOfWeek.Sunday);
Output:
Sunday 01 1 Sunday
4. C# Enum Instance methods
C# doesn’t provide many instance methods due to the simplicity of its enum. Let’s go through them.
4.1 int CompareTo(object target)
Compares this instance to a specified object and returns an indication of their relative values.
- Parameter: target – an object to compare, or null.
- Returns: a signed integer: less than zero if the value of this instance is less than the value of target. Zero if the value of this instance is equal to the value of target. Greater than zero if the value of this instance is greater than the value of target, or target is
null
. - Throws:
ArgumentException
if target and this instance are not the same type. - Throws:
if value is not of type SByte, Int16, Int32, Int64, Byte, UInt16, UInt32, or UInt64.InvalidOperationException
- Throws:
NullReferenceException
this instance isnull
.
private static void CompareTo(DaysOfWeek enum1, DaysOfWeek enum2) { Console.WriteLine("Is {0} same as {1}: {2}", enum1, enum2, enum1.CompareTo(enum2) == 0); // If enum1 == enum2 = 0 Console.WriteLine("Is {0} after {1}: {2}", enum1, enum2, enum1.CompareTo(enum2) > 0); // If enum1 > enum2 = +1 Console.WriteLine("Is {0} before {1}: {2}", enum1, enum2, enum1.CompareTo(enum2) < 0); // If enum1 < enum2 = -1 Console.WriteLine(); }
Main method call:
CompareTo(DaysOfWeek.Monday, DaysOfWeek.Saturday); //Returns -1 CompareTo(DaysOfWeek.Sunday, DaysOfWeek.Sunday); //Returns 0 CompareTo(DaysOfWeek.Saturday, DaysOfWeek.Friday); //Returns +1
Output:
Is Monday same as Saturday: False Is Monday after Saturday: False Is Monday before Saturday: True Is Sunday same as Sunday: True Is Sunday after Sunday: False Is Sunday before Sunday: False Is Saturday same as Friday: False Is Saturday after Friday: True Is Saturday before Friday: False
4.2 string ToString(string format)
Converts the value of this instance to its equivalent string representation using the specified format.
- Parameter: format – a format string. Same as in the format method in section 3.11.
- Returns: the string representation of the value of this instance as specified by format.
- Throws:
format does not contain a valid format specification.FormatException
- Throws:
InvalidOperationException
format equals "X", but the enumeration type is unknown.
If you do not provide a format, that is, if you pass an empty string or null
, it will use “G” by default.
private static void ToString(DaysOfWeek day, string format) { Console.WriteLine(day.ToString(format)); }
Main method call:
InstanceMethods.ToString(DaysOfWeek.Wednesday, "G"); InstanceMethods.ToString(DaysOfWeek.Wednesday, "D"); InstanceMethods.ToString(DaysOfWeek.Wednesday, "X"); InstanceMethods.ToString(DaysOfWeek.Wednesday, "F"); InstanceMethods.ToString(DaysOfWeek.Wednesday, ""); // Same as "G" InstanceMethods.ToString(DaysOfWeek.Wednesday, null); // Same as "G" //InstanceMethods.ToString(DaysOfWeek.Wednesday, "A"); // Throw FormatException
Output:
Wednesday 4 04 Wednesday Wednesday Wednesday
4.3 bool HasFlag(Enum flag)
Determines whether one or more bit-fields are set in the current instance.
- Parameter: flag – an enumeration value.
- Returns:
true
if the bit field or bit fields that are set in flag are also set in the current instance; otherwise,false
. - Throws:
flag is a different type than the current instance.ArgumentException
internal static void HasFlag() { // Becuase Saturday is associated with the the value 7, 0111 in binary, // HasFlag will always return true because every other value from 0 to 7 is within 0111, var saturday = DaysOfWeek.Saturday; foreach (DaysOfWeek day in Enum.GetValues(typeof(DaysOfWeek))) { Console.WriteLine("Does {0} have flags on {1}: {2}", saturday, day, saturday.HasFlag(day)); } Console.WriteLine(); // Because Unknown is associated with the value 0 (zero), 0000 in binary, // HasFlag will only return true for itself (Unknown). var unknown = DaysOfWeek.Unknown; foreach (DaysOfWeek day in Enum.GetValues(typeof(DaysOfWeek))) { Console.WriteLine("Does {0} have flags on {1}: {2}", unknown, day, unknown.HasFlag(day)); } Console.WriteLine(); // Because Tuesday is associated with the value 3, 0011 in binary, // HasFlag will return true for Unknown, Monday & Tuesday var tuesday = DaysOfWeek.Tuesday; foreach (DaysOfWeek day in Enum.GetValues(typeof(DaysOfWeek))) { Console.WriteLine("Does {0} have flags on {1}: {2}", tuesday, day, tuesday.HasFlag(day)); } }
Output:
Does Saturday have flags on Unknown: True Does Saturday have flags on Sunday: True Does Saturday have flags on Monday: True Does Saturday have flags on Tuesday: True Does Saturday have flags on Wednesday: True Does Saturday have flags on Thursday: True Does Saturday have flags on Friday: True Does Saturday have flags on Saturday: True Does Unknown have flags on Unknown: True Does Unknown have flags on Sunday: False Does Unknown have flags on Monday: False Does Unknown have flags on Tuesday: False Does Unknown have flags on Wednesday: False Does Unknown have flags on Thursday: False Does Unknown have flags on Friday: False Does Unknown have flags on Saturday: False Does Tuesday have flags on Unknown: True Does Tuesday have flags on Sunday: True Does Tuesday have flags on Monday: True Does Tuesday have flags on Tuesday: True Does Tuesday have flags on Wednesday: False Does Tuesday have flags on Thursday: False Does Tuesday have flags on Friday: False Does Tuesday have flags on Saturday: False
4.3.1 How Flags Work
Flags work differently from regular enums in such a way that if you provide a value that is not directly associated with an enum
value, but whose value is the sum of two or more enums values, it will automatically return all of them. This happens because instead of comparing the values themselves, the comparison is done using bitwise OR. To make the most of Flags, you should provide values resulting from the binary system, that is, of base two to the power of n. See picture below.
To better understand how Flags work, let’s create another enum and name its file Binary.cs
.
using System; namespace Enumeration { [Flags] internal enum Binary : byte { One = 1, Two = 2, Four = 4, Eight = 8, Sixteen = 16, ThirtyTwo = 32, SixtyFour = 64, HundredTwentyEight = 128, } }
Now let’s add a method to loop from 0 to 128 incrementing by two.
private static void PrintEnumFlags() { for (int i = 0; i <= 128; i += 2) { Console.WriteLine("{0,3} - {1:G}", i, (Binary)i); } }
Output:
0 - 0 2 - Two 4 - Four 6 - Two, Four 8 - Eight 10 - Two, Eight 12 - Four, Eight 14 - Two, Four, Eight 16 - Sixteen 18 - Two, Sixteen 20 - Four, Sixteen 22 - Two, Four, Sixteen 24 - Eight, Sixteen 26 - Two, Eight, Sixteen 28 - Four, Eight, Sixteen 30 - Two, Four, Eight, Sixteen 32 - ThirtyTwo 34 - Two, ThirtyTwo 36 - Four, ThirtyTwo 38 - Two, Four, ThirtyTwo 40 - Eight, ThirtyTwo 42 - Two, Eight, ThirtyTwo 44 - Four, Eight, ThirtyTwo 46 - Two, Four, Eight, ThirtyTwo 48 - Sixteen, ThirtyTwo 50 - Two, Sixteen, ThirtyTwo 52 - Four, Sixteen, ThirtyTwo 54 - Two, Four, Sixteen, ThirtyTwo 56 - Eight, Sixteen, ThirtyTwo 58 - Two, Eight, Sixteen, ThirtyTwo 60 - Four, Eight, Sixteen, ThirtyTwo 62 - Two, Four, Eight, Sixteen, ThirtyTwo 64 - SixtyFour 66 - Two, SixtyFour 68 - Four, SixtyFour 70 - Two, Four, SixtyFour 72 - Eight, SixtyFour 74 - Two, Eight, SixtyFour 76 - Four, Eight, SixtyFour 78 - Two, Four, Eight, SixtyFour 80 - Sixteen, SixtyFour 82 - Two, Sixteen, SixtyFour 84 - Four, Sixteen, SixtyFour 86 - Two, Four, Sixteen, SixtyFour 88 - Eight, Sixteen, SixtyFour 90 - Two, Eight, Sixteen, SixtyFour 92 - Four, Eight, Sixteen, SixtyFour 94 - Two, Four, Eight, Sixteen, SixtyFour 96 - ThirtyTwo, SixtyFour 98 - Two, ThirtyTwo, SixtyFour 100 - Four, ThirtyTwo, SixtyFour 102 - Two, Four, ThirtyTwo, SixtyFour 104 - Eight, ThirtyTwo, SixtyFour 106 - Two, Eight, ThirtyTwo, SixtyFour 108 - Four, Eight, ThirtyTwo, SixtyFour 110 - Two, Four, Eight, ThirtyTwo, SixtyFour 112 - Sixteen, ThirtyTwo, SixtyFour 114 - Two, Sixteen, ThirtyTwo, SixtyFour 116 - Four, Sixteen, ThirtyTwo, SixtyFour 118 - Two, Four, Sixteen, ThirtyTwo, SixtyFour 120 - Eight, Sixteen, ThirtyTwo, SixtyFour 122 - Two, Eight, Sixteen, ThirtyTwo, SixtyFour 124 - Four, Eight, Sixteen, ThirtyTwo, SixtyFour 126 - Two, Four, Eight, Sixteen, ThirtyTwo, SixtyFour 128 - HundredTwentyEight
By looking at the results you can see that although many of the numbers were not in our enum
, a combination that resulted in the value of the enum
was shown Let’s take the number 6 in binary, for instance, it is 00000110 which is not associated with any enum
constant. However, the number 4, 00000100, and 2, 00000010, are and add up to 6.
4.3.2 Flag Operators | OR
To understand how powerful Flags
are, let’s add another enum
named Weekday, but this time their values will be based on the binary system. You should always use base two binary system whenever using Flags
.
using System; namespace Enumeration { [Flags] internal enum Weekday { Monday = 1, Tuesday = 2, Wednesday = 4, Thursday = 8, Friday = 16, Saturday = 32, Sunday = 64, // Value will be 96 (64 + 32) Weekend = Saturday | Sunday, // Value will be 31 (16 + 8 + 4 + 2 + 1) BusinessDays = Monday | Tuesday | Wednesday | Thursday | Friday, } internal static class WeekdayExtension { public static bool IsWeekend(this Weekday e, Weekday day) { return Weekday.Weekend.HasFlag(day); } public static bool IsBusinessDay(this Weekday e, Weekday day) { return Weekday.BusinessDays.HasFlag(day); } } }
As you can see, we also added two extension methods, one to check whether a day is a business day and another to check whether the day is a weekend day. Check how much simpler it is to do so using Flags
.
private static void PrintWeekdays() { foreach (Weekday day in Enum.GetValues(typeof(Weekday))) { Console.WriteLine("{0} value is {1}", day, (int)day); Console.WriteLine("{0} is a Business day: {1}", day, day.IsBusinessDay(day)); Console.WriteLine("{0} is a Weekend day: {1}", day, day.IsWeekend(day)); Console.WriteLine(); } }
Output:
Monday value is 1 Monday is a Business day: True Monday is a Weekend day: False Tuesday value is 2 Tuesday is a Business day: True Tuesday is a Weekend day: False Wednesday value is 4 Wednesday is a Business day: True Wednesday is a Weekend day: False Thursday value is 8 Thursday is a Business day: True Thursday is a Weekend day: False Friday value is 16 Friday is a Business day: True Friday is a Weekend day: False BusinessDays value is 31 BusinessDays is a Business day: True BusinessDays is a Weekend day: False Saturday value is 32 Saturday is a Business day: False Saturday is a Weekend day: True Sunday value is 64 Sunday is a Business day: False Sunday is a Weekend day: True Weekend value is 96 Weekend is a Business day: False Weekend is a Weekend day: True
4.3.3 Flag Operator & AND
We can use the & operator to check if a certain day is contained within an array for example. When we use & for bitwise comparison, it will return one for those bits only if both are 1, otherwise zero. Since we are using base two for our enum
, there is only one bit 1 for each member and any comparison except with itself or cluster where this very day in contained will return zero, which is not valid in our enum
. Let’s add another extension method.
public static bool Contains(this Weekday[] weekdays, Weekday day) { foreach (Weekday d in weekdays) { if ((day & d) != 0) return true; } return false; }
Here we have a static extension method that will be available for any array of Weekday, and it takes one parameter, a Weekday. Now let’s add another method that will loop through every single day in our Weekday enum and check if array contains any of the weekdays.
private static void ContainWeekdays(Weekday[] weekdays) { foreach (Weekday day in Enum.GetValues(typeof(Weekday))) { Console.WriteLine("Does the Array contain {0}? {1}", day, weekdays.Contains(day)); } Console.WriteLine(); }
Now, for our Main method, let’s call this method passing different arrays to make sure our method really works as intended.
ContainWeekdays(new Weekday[] { Weekday.BusinessDays, Weekday.Weekend }); // All true ContainWeekdays(new Weekday[] { 0, (Weekday)128 }); // All false ContainWeekdays(new Weekday[] { Weekday.Monday }); // true for Monday & BusinessDay ContainWeekdays(new Weekday[] { Weekday.Sunday }); // true for Sunday & Weekend
Output:
Is Monday contained in the Array: True Is Tuesday contained in the Array: True Is Wednesday contained in the Array: True Is Thursday contained in the Array: True Is Friday contained in the Array: True Is BusinessDays contained in the Array: True Is Saturday contained in the Array: True Is Sunday contained in the Array: True Is Weekend contained in the Array: True Is Monday contained in the Array: False Is Tuesday contained in the Array: False Is Wednesday contained in the Array: False Is Thursday contained in the Array: False Is Friday contained in the Array: False Is BusinessDays contained in the Array: False Is Saturday contained in the Array: False Is Sunday contained in the Array: False Is Weekend contained in the Array: False Is Monday contained in the Array: True Is Tuesday contained in the Array: False Is Wednesday contained in the Array: False Is Thursday contained in the Array: False Is Friday contained in the Array: False Is BusinessDays contained in the Array: True Is Saturday contained in the Array: False Is Sunday contained in the Array: False Is Weekend contained in the Array: False Is Monday contained in the Array: False Is Tuesday contained in the Array: False Is Wednesday contained in the Array: False Is Thursday contained in the Array: False Is Friday contained in the Array: False Is BusinessDays contained in the Array: False Is Saturday contained in the Array: False Is Sunday contained in the Array: True Is Weekend contained in the Array: True
4.3.4 Flag Operator ^ XOR
We can use the ^ XOR operator to check for equality, for instance. When we use ^ for bitwise comparison, equal bits turn into 0 and different bits into 1, that is, if two enums are equal, it will result in 0. Let’s add our last extension method to compare for equality using ^ XOR operator.
public static bool IsEqualsTo(this Weekday day, Weekday otherDay) { return (day ^ otherDay) == 0; }
Quite simple, yet very powerful and efficient code.
private static void EqualsWeekday(Weekday otherDay) { foreach (Weekday day in Enum.GetValues(typeof(Weekday))) { Console.WriteLine("Is {0} equals to {1}? {2}", day, otherDay, day.IsEqualsTo(otherDay)); } Console.WriteLine(); }
Main method call:
EqualsWeekday(Weekday.Monday); EqualsWeekday(Weekday.Weekend); EqualsWeekday(Weekday.BusinessDays);
Output:
Is Monday equals to Monday? True Is Tuesday equals to Monday? False Is Wednesday equals to Monday? False Is Thursday equals to Monday? False Is Friday equals to Monday? False Is BusinessDays equals to Monday? False Is Saturday equals to Monday? False Is Sunday equals to Monday? False Is Weekend equals to Monday? False Is Monday equals to Weekend? False Is Tuesday equals to Weekend? False Is Wednesday equals to Weekend? False Is Thursday equals to Weekend? False Is Friday equals to Weekend? False Is BusinessDays equals to Weekend? False Is Saturday equals to Weekend? False Is Sunday equals to Weekend? False Is Weekend equals to Weekend? True Is Monday equals to BusinessDays? False Is Tuesday equals to BusinessDays? False Is Wednesday equals to BusinessDays? False Is Thursday equals to BusinessDays? False Is Friday equals to BusinessDays? False Is BusinessDays equals to BusinessDays? True Is Saturday equals to BusinessDays? False Is Sunday equals to BusinessDays? False Is Weekend equals to BusinessDays? False
4.4 TypeCode GetTypeCode()
Returns the type code of the underlying type of this enumeration member.
- Returns: The type code of the underlying type of this instance.
- Throws:
if the enumeration type is unknown.InvalidOperationException
internal static void GetTypeCode() { DaysOfWeek thursday = DaysOfWeek.Thursday; Console.WriteLine("{0} type code is {1}.", thursday, thursday.GetTypeCode()); }
Output:
Thursday type code is Byte.
5. Enhanced Enum in C#
Also sometimes called Java Like Enum is a recurring request from the C# community and from developers who have switched from Java to C#, and it is because enum
in C# isn’t as flexible as Java’s, especially because you cannot have logic in it, well, not directly, but there are some walkarounds we can do.
Luckily, there are some ways one can achieve that in C#, so let’s go through each one of them.
5.1 Using Extension Methods
One interesting feature in C# is the possibility of creating extension methods and it works even for native classes, such as primitive wrappers. Let’s say we needed a method to handle printing out the correct enum
day to console. Since Enumeration in C# does not allow methods, we would have to create a helper method that takes out enum
type as a parameter, which is not a bad solution, but you will need a separate helper class and then type it every time you need to print it out.
Let’s add a new class named EnumExtension
and make it static
then add a method Print
to it. Note that we must use the this
keyword so the compiler knows where this static method belongs to.
using System; namespace Enumeration { static class EnumExtension { public static void Print(this DaysOfWeek day) { switch (day) { case DaysOfWeek.Sunday: Console.WriteLine("{0} is the {1}st day of the Week!", day, (byte)day); break; case DaysOfWeek.Monday: Console.WriteLine("{0} is the {1}nd day of the Week!", day, (byte)day); break; case DaysOfWeek.Tuesday: Console.WriteLine("{0} is the {1}rd day of the Week!", day, (byte)day); break; case DaysOfWeek.Wednesday: case DaysOfWeek.Thursday: case DaysOfWeek.Friday: case DaysOfWeek.Saturday: Console.WriteLine("{0} is the {1}th day of the Week!", day, (byte)day); break; default: Console.WriteLine("Sorry, not a day"); break; } } } }
Now, in our Print method, let’s create some enums then test our new enum
static method Print like so.
private static void Print() { DaysOfWeek unk = DaysOfWeek.Unknown; DaysOfWeek sun = DaysOfWeek.Sunday; DaysOfWeek mon = DaysOfWeek.Monday; DaysOfWeek tue = DaysOfWeek.Tuesday; DaysOfWeek sat = DaysOfWeek.Saturday; unk.Print(); sun.Print(); mon.Print(); tue.Print(); sat.Print(); }
Output:
Sorry, not a day Sunday is the 1st day of the Week! Monday is the 2nd day of the Week! Tuesday is the 3rd day of the Week! Saturday is the 7th day of the Week!
5.2 Using Data Annotations
It is possible to use Data Annotation Attributes to add a description, name, or any other data for that matter. The most common Attribute
is Display
, but should you need something more specific, you can add your own custom attributes.
The drawback is that you won’t have methods out-of-the-box to retrieve them, and you will have to use Reflection and add your own custom methods to work with your data.
using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Reflection; namespace Enumeration.Enums { // Enum Package with the out-of-the-box Display Attribute and two custom Attributes Weight and Size internal enum Package { [Weight(10)] [Size("15x15x20")] [Display(Name = "Small Package")] Small = 1, [Weight(20)] [Size("20x20x30")] [Display(Name = "Medium Package")] Medium = 2, [Weight(35)] [Size("25x25x40")] [Display(Name = "Large Package")] Large = 3, [Weight(50)] [Size("30x30x50")] [Display(Name = "Extra Large Package")] XLarge = 4, } // Double Attribute Weight internal class WeightAttribute : Attribute { public readonly double Weight; public WeightAttribute(double weight) { Weight = weight; } } // String Attribute Size internal class SizeAttribute : Attribute { public readonly string Size; public SizeAttribute(string size) { Size = size; } } internal static class EnumPackage { // Retrieves the Enum Display should you need to print it. internal static string GetEnumDisplayName(this Package value) { return GetMember(value).GetCustomAttribute<DisplayAttribute>().Name; } // Retrieves the Enum Wright should you need to print it. internal static string GetEnumWeight(this Package value) { // Because this Attribute returns double, we need to use the ToString() method. return GetMember(value).GetCustomAttribute<WeightAttribute>().Weight.ToString(); } // Retrieves the Enum Size should you need to print it. internal static string GetEnumSize(this Package value) { return GetMember(value).GetCustomAttribute<SizeAttribute>().Size; } internal static string GetEnumDescriptionName(this ErrorMessage value) { return GetMember(value).GetCustomAttribute<DescriptionAttribute>().Description; } private static MemberInfo GetMember(Enum value) { Type type = value.GetType(); return type.GetMember(value.ToString())[0]; } internal static Package EnumFromDisplayAttribute(string displayValue) { MemberInfo[] members = GetMembers(typeof(Package)); if (members.Length > 0) { for (int i = 0; i < members.Length; i++) { if (Enum.TryParse(members[i].Name, out Package e)) { // If our Display Attribute matches the parameter displayValue it means it exists. if (members[i].GetCustomAttribute<DisplayAttribute>().Name.Equals(displayValue)) return e; } } } return 0; } internal static Package EnumFromWeightAttribute(double weightValue) { MemberInfo[] members = GetMembers(typeof(Package)); if (members.Length > 0) { for (int i = 0; i < members.Length; i++) { if (Enum.TryParse(members[i].Name, out Package e)) { // If our Weight Attribute matches the parameter weightValue it means it exists. if (members[i].GetCustomAttribute<WeightAttribute>().Weight.Equals(weightValue)) return e; } } } return 0; } internal static Package EnumFromSizeAttribute(string sizeValue) { MemberInfo[] members = GetMembers(typeof(Package)); if (members.Length > 0) { for (int i = 0; i < members.Length; i++) { if (Enum.TryParse(members[i].Name, out Package e)) { // If our Weight Attribute matches the parameter sizeValue it means it exists. if (members[i].GetCustomAttribute<SizeAttribute>().Size.Equals(sizeValue)) return e; } } } return 0; } private static MemberInfo[] GetMembers(Type type) { // BindingFlags to only bring the enum constants, otherwise it'd bring all members. return type.GetMembers(BindingFlags.Static | BindingFlags.Public); } } }
We have created a simple enum
Package with 3 attributes, two of which are custom. We also added some extension methods to print out the attribute values and some static methods to be able to retrieve an enum
based on any of its attributes. This requires a lot of reflection and parsing.
Here is our Main method call:
private static void PrintPackages() { foreach (Package e in Enum.GetValues(typeof(Package))) { Console.WriteLine(e.GetEnumDisplayName()); Console.WriteLine(e.GetEnumWeight()); Console.WriteLine(e.GetEnumSize()); Console.WriteLine(); } }
Output:
Small Package 10 15x15x20 Medium Package 20 20x20x30 Large Package 35 25x25x40 Extra Large Package 50 30x30x50
We will add some more methods just for the purpose of formatting and allowing for different entries.
private static void EnumFromDisplayAttribute(string att) { Package p; if ((p = EnumPackage.EnumFromDisplayAttribute(att)) != 0) { Console.WriteLine("Attribute found in {0}: {1}.", p, att); } else { Console.WriteLine("Attribute NOT found in {0}: {1}.", p.GetType().Name, att); } } private static void EnumFromWeightAttribute(double att) { Package p; if ((p = EnumPackage.EnumFromWeightAttribute(att)) != 0) { Console.WriteLine("Attribute found in {0}: {1}.", p, att); } else { Console.WriteLine("Attribute NOT found in {0}: {1}.", p.GetType().Name, att); } } private static void EnumFromSizeAttribute(string att) { Package p; if ((p = EnumPackage.EnumFromSizeAttribute(att)) != 0) { Console.WriteLine("Attribute found in {0}: {1}.", p, att); } else { Console.WriteLine("Attribute NOT found in {0}: {1}.", p.GetType().Name, att); } }
Now it is time to test out our methods. Here is the Main method calls:
EnumFromDisplayAttribute("Small Package"); EnumFromDisplayAttribute("Extra Large Package"); EnumFromDisplayAttribute("Tiny Package"); Console.WriteLine(); EnumFromWeightAttribute(20); EnumFromWeightAttribute(35); EnumFromWeightAttribute(5); Console.WriteLine(); EnumFromSizeAttribute("25x25x40"); EnumFromSizeAttribute("30x30x50"); EnumFromSizeAttribute("10x10x15");
Output:
Attribute found in Small: Small Package. Attribute found in XLarge: Extra Large Package. Attribute NOT found in Package: Tiny Package. Attribute found in Medium: 20. Attribute found in Large: 35. Attribute NOT found in Package: 5. Attribute found in Large: 25x25x40. Attribute found in XLarge: 30x30x50. Attribute NOT found in Package: 10x10x15.
5.2.1 Using Component Model Attribute
This is not much different from the previous approach, but to make things more interesting, let’s say you have an enum
with the Flags
attribute because you may want to return two or more of them and to do so, we are going to have to tweak our methods a little bit to return one or more messages accordingly.
using System; using System.ComponentModel; using System.Reflection; namespace Enumeration.Enums { [Flags] internal enum ErrorMessage { [Description("This username is already taken! Please choose another one.")] TakenUsername = 1, [Description("The password is too weak. It must be at least 8 characters long.")] WeakPassword = 2, [Description("Fields marked with * are mandatory.")] MandatoryFields = 4, [Description("provide a valid date dd/MM/yyyy")] InvalidDate = 8 } internal static class ErrorMessageExtension { private static MemberInfo[] GetMembers(ErrorMessage value) { ErrorMessage[] errors = (ErrorMessage[])Enum.GetValues(typeof(ErrorMessage)); MemberInfo[] memberInfos = new MemberInfo[errors.Length]; for (int i = 0; i < errors.Length; i++) { if ((errors[i] & value) != 0) { Type type = value.GetType(); memberInfos[i] = type.GetMember(errors[i].ToString())[0]; } } return memberInfos; } internal static string[] GetEnumDescriptions(this ErrorMessage value) { MemberInfo[] members = GetMembers(value); string[] errors = new string[members.Length]; for (int i = 0; i < members.Length && members[i] != null; i++) { errors[i] = members[i].GetCustomAttribute<DescriptionAttribute>().Description; } return errors; } } }
Now let’s add a method to print it out to the console.
private static void PrintErrorMessages(ErrorMessage e) { // The join() method returns a string that consists of all the elements in the array. // separated by the delimiter specified, first argument, in this case, a new liner. Console.WriteLine(string.Join("\n", e.GetEnumDescriptions())); }
Main method call:
PrintErrorMessages(ErrorMessage.TakenUsername | ErrorMessage.WeakPassword); // Instead of adding all four, which would take up a lot of space, we are using binary. // When cast to ErrorMessage, it will contain all mesasges PrintErrorMessages((ErrorMessage)0b000_1111);
Output:
This username is already taken! Please choose another one. The password is too weak. It must be at least 8 characters long. This username is already taken! Please choose another one. The password is too weak. It must be at least 8 characters long. Fields marked with * are mandatory. provide a valid date dd/MM/yyyy
5.3 Using Sealed Classes
There will be times when extension methods won’t cut it because you need a lot more than just a couple of methods. You may need fields to store data and perform some calculations. Since we started off with a quite simple DaysOfWeek
enum
, let’s add another one so we can do some fancy stuff with it. First let’s create a sealed class and call it Continents.
using System; using System.Reflection; namespace Enumeration { internal sealed class Continents { public byte Value { get; private set; } public string Name { get; private set; } public long Area { get; private set; } public long Population { get; private set; } private Continents(byte value, string name, long area, long population) { Value = value; Name = name; Area = area; Population = population; } public static readonly Continents Asia = new Continents(1, "Asia", 44_579_000, 4_721_383_274); public static readonly Continents Africa = new Continents(2, "Africa", 30_370_000, 1_426_730_933); public static readonly Continents NorthAmerica = new Continents(3, "North America", 24_709_000, 600_296_136); public static readonly Continents SouthAmerica = new Continents(4, "South America", 17_840_000, 436_816_608); public static readonly Continents Antarctica = new Continents(5, "Antarctica", 14_200_000, 4_490); public static readonly Continents Europe = new Continents(6, "Europe", 10_180_000, 743_147_538); public static readonly Continents Oceania = new Continents(7, "Oceania", 8_600_000, 26_180_437); public static Continents[] GetValues() { // We will be using reflection to get all public static Fields in Continents. FieldInfo[] fields = typeof(Continents).GetFields(BindingFlags.Static | BindingFlags.Public); Continents[] continents = new Continents[fields.Length]; for (int i = 0; i < fields.Length; i++) { FieldInfo field = fields[i]; if (field.IsInitOnly) // Checks whether this field can only be set by a Constructor. { continents[i] = (Continents)field.GetValue(typeof(Continents)); } } return continents; } public static Continents StringToContinent(string continent) { foreach (var cont in GetValues()) { if (cont.Name == continent) return cont; } return null; } public static Continents ValueToContinent(byte value) { foreach (var cont in GetValues()) { if (cont.Value == value) return cont; } throw new ArgumentException("Value does not correspond to any Continent!"); } public override string ToString() { return String.Format("{0} has an area of {1} and its current population is {2}", this.Name, this.Area, this.Population); } } internal static class Extension { public static double GetDensity(this Continents c) { return c.Population / c.Area; } public static double GetPopulatioPercentage(this Continents c) { double total = 0; foreach (var continent in typeof(Continents).GetFields(BindingFlags.Static | BindingFlags.Public)) { if (continent.IsInitOnly) { Continents cont = (Continents)continent.GetValue(typeof(Continents)); total += cont.Population; } } return c.Population / total * 100; } public static double GetAreaPercentage(this Continents c) { double total = 0; foreach (var continent in typeof(Continents).GetFields(BindingFlags.Static | BindingFlags.Public)) { if (continent.IsInitOnly) { Continents cont = (Continents)continent.GetValue(typeof(Continents)); total += cont.Area; } } return c.Area / total * 100; } } }
First, we added 4 Properties with private
sets, then we added a private
constructor, after that we instantiated seven static
readonly
objects of Continents itself and named them after each continent, and since we are no longer using enums, we won’t be able to use the static
methods from the abstract Enum class
, so we will have to create them ourselves if we ever need them. Finally, we added an internal static class
Extension which will provide us with some handy methods.
private static void EnhancedEnum() { // Let's test our method to retrieve all the Continents foreach (var c in Continents.GetValues()) { Console.WriteLine(c); // Because we overridden the ToString() method we don't need to do it here. Console.WriteLine("{0} density is {1:##0.00}", c.Name, c.GetDensity()); Console.WriteLine("{0} represents {1:##0.00} of the global population.", c.Name, c.GetPopulatioPercentage()); Console.WriteLine("{0} represents {1:##0.00} of the global area.", c.Name, c.GetAreaPercentage()); Console.WriteLine(); } }
As you can see, our enum
like class works splendidly well, especially when combined with extension methods.
Output:
Asia has an area of 44579000 and its current population is 4721383274 Asia density is 105.00 Asia represents 59.35 of the global population. Asia represents 29.62 of the global area. Africa has an area of 30370000 and its current population is 1426730933 Africa density is 46.00 Africa represents 17.94 of the global population. Africa represents 20.18 of the global area. North America has an area of 24709000 and its current population is 600296136 North America density is 24.00 North America represents 7.55 of the global population. North America represents 16.42 of the global area. South America has an area of 17840000 and its current population is 436816608 South America density is 24.00 South America represents 5.49 of the global population. South America represents 11.86 of the global area. Antarctica has an area of 14200000 and its current population is 4490 Antarctica density is 0.00 Antarctica represents 0.00 of the global population. Antarctica represents 9.44 of the global area. Europe has an area of 10180000 and its current population is 743147538 Europe density is 73.00 Europe represents 9.34 of the global population. Europe represents 6.77 of the global area. Oceania has an area of 8600000 and its current population is 26180437 Oceania density is 3.00 Oceania represents 0.33 of the global population. Oceania represents 5.72 of the global area.
Now let’s test the other methods we have created in our Continents class.
internal static void MoreEnhancedEnum() { Continents northAmericaStr = Continents.StringToContinent("North America"); // return null if not found. Continents northAmerica = Continents.NorthAmerica; Console.WriteLine("{0} is equal to {1}", northAmerica.Name, northAmericaStr.Name); Console.WriteLine(northAmerica == northAmericaStr); // Should yield true. Console.WriteLine(); Continents africa = Continents.Africa; Continents africaVal = Continents.ValueToContinent(2); // Africa's value is 2 //Continents unknown = Continents.ValueToContinent(0); // UNCOMMENT TO THROW AN EXCEPTION Console.WriteLine("{0} is equal to {1}", africa.Name, africaVal.Name); Console.WriteLine(northAmerica == northAmericaStr); // Should yield true. }
Output:
North America is equal to North America True Africa is equal to Africa True
5.4 Using Abstract Classes
Another way to achieve advanced enum
functionality is by using an abstract class. It is flexible, more maintainable and allows for a neater solution. We will make use of a lot of Object-Oriented concepts, and it is especially useful when you need your constants to have different behavior.
Let’s start by adding an Abstract Enum class to hold everything that is common to every member in our enum
.
using System; namespace Enumeration { public abstract class AbstractEnum { public enum Compensation { None = 0, Week = 7, Fortnight = 15, Month = 30 } public byte Code { get; private set; } public string Position { get; private set; } public double HourPay { get; private set; } protected AbstractEnum(byte code, string position, double hourPay) { Code = code; Position = position; HourPay = hourPay; } // virtual offers a default implementation but allows for method-overridden should you need it. public virtual double GetBonus() { // Here the default impletation is to throw an exception, thus forcing child classes to implement it. throw new NotImplementedException(); } // Must be overridden. Doesn't provide any default implementation protected internal abstract Compensation GetVacationCompensation(); /// <summary> /// Calculates the salary of the employee. /// </summary> /// <param name="hours">How many hours the employee has worked in a given period of time.</param> /// <returns>The value of the hour (according to the position) times hours</returns> public double GetSalary(double hours) { return HourPay * hours; } public override string ToString() { return Position.ToString(); } } }
Now it is time to add our actual DeveloperLevel
enum Class, the one we will be using as our enum
. Here, each constant will be a private inner class that extends the DeveloperLevel
Class.
using System.Reflection; namespace Enumeration { public abstract class DeveloperLevel : AbstractEnum { private static readonly double TraineeBonusRate = 0.01; private static readonly double JuniorBonusRate = 0.02; private static readonly double MidLevelBonusRate = 0.03; private static readonly double SeniorBonusRate = 0.04; private static readonly double LeadBonusRate = 0.05; private static readonly double Factor = 100; private DeveloperLevel(byte code, string position, double hourPay) : base(code, position, hourPay) { } public static readonly DeveloperLevel Trainee = new TraineeLevel(1, "Trainee", 5.00); public static readonly DeveloperLevel Junior = new JuniorLevel(2, "Junior", 10.00); public static readonly DeveloperLevel MidLevel = new MidLevelLevel(3, "Mid-Level", 15.00); public static readonly DeveloperLevel Senior = new SeniorLevel(4, "Senior", 20.00); public static readonly DeveloperLevel Lead = new LeadLevel(4, "Lead", 25.00); public static DeveloperLevel[] GetLevels() { // We will be using reflection to get all public static Fields in Continents. FieldInfo[] fields = typeof(DeveloperLevel).GetFields(BindingFlags.Static | BindingFlags.Public); DeveloperLevel[] levels = new DeveloperLevel[fields.Length]; for (int i = 0; i < fields.Length; i++) { FieldInfo field = fields[i]; if (field.IsInitOnly) // Checks whether this field can only be set by a Constructor. { levels[i] = (DeveloperLevel)field.GetValue(typeof(DeveloperLevel)); } } return levels; } private class TraineeLevel : DeveloperLevel { protected internal TraineeLevel(byte code, string position, double hourPay) : base(code, position, hourPay) { } protected internal override Compensation GetVacationCompensation() { return Compensation.None; } public override double GetBonus() { return HourPay * TraineeBonusRate * Factor; } } private class JuniorLevel : DeveloperLevel { protected internal JuniorLevel(byte code, string position, double hourPay) : base(code, position, hourPay) { } protected internal override Compensation GetVacationCompensation() { return Compensation.None; } public override double GetBonus() { return HourPay * JuniorBonusRate * Factor; } } private class MidLevelLevel : DeveloperLevel { protected internal MidLevelLevel(byte code, string position, double hourPay) : base(code, position, hourPay) { } protected internal override Compensation GetVacationCompensation() { return Compensation.Week; } public override double GetBonus() { return HourPay * MidLevelBonusRate * Factor; } } private class SeniorLevel : DeveloperLevel { protected internal SeniorLevel(byte code, string position, double hourPay) : base(code, position, hourPay) { } protected internal override Compensation GetVacationCompensation() { return Compensation.Fortnight; } public override double GetBonus() { return HourPay * SeniorBonusRate * Factor; } } private class LeadLevel : DeveloperLevel { protected internal LeadLevel(byte code, string position, double hourPay) : base(code, position, hourPay) { } protected internal override Compensation GetVacationCompensation() { return Compensation.Month; } public override double GetBonus() { return HourPay * LeadBonusRate + Factor; } } } }
Main method call:
private static void PrintDeveloperLevels() { foreach (var level in DeveloperLevel.GetLevels()) { Console.WriteLine("Developer Level: {0}.", level); // How much each level would get paid weekly working 44 hours. Console.WriteLine("Salary: {0:##0.00}.", level.GetSalary(44)); Console.WriteLine("Bonus: {0:##0.00}.", level.GetBonus()); Console.WriteLine("Vacation Compensation Period: {0}.", level.GetVacationCompensation()); Console.WriteLine(); } }
Output:
Developer Level: Trainee. Salary: 220.00. Bonus: 5.00. Vacation Compensation Period: None. Developer Level: Junior. Salary: 440.00. Bonus: 20.00. Vacation Compensation Period: None. Developer Level: Mid-Level. Salary: 660.00. Bonus: 45.00. Vacation Compensation Period: Week. Developer Level: Senior. Salary: 880.00. Bonus: 80.00. Vacation Compensation Period: Fortnight. Developer Level: Lead. Salary: 1100.00. Bonus: 101.25. Vacation Compensation Period: Month.
5.5 Using NuGet Packages
NuGet or .NET Package Manager is designed to enable developers to share reusable code. There is one called SmartEnum by Ardalis which is neat, and it is quite simple to use. You just need to download and install it and you are good to go. It offers a default implementation, and its constructor takes two parameters, a string name, and an int value, which will suffice for most cases. You can add new properties, but if what you need is specific, you’d better stick with any of the previous solutions.
After installation, you will need to import it to your class like so: using Ardalis.SmartEnum;
after that, all you need to do is to extend SmartEnum
and, since it is a generic type, you will have to provide your class identifier between angle brackets <>.
using Ardalis.SmartEnum; namespace Enumeration { internal class ErrorSmartEnum : SmartEnum<ErrorSmartEnum> { protected internal ErrorSmartEnum(string name, int value) : base(name, value) { } public static readonly ErrorSmartEnum Invalid_ID = new ErrorSmartEnum("Invalid ID.", 1); public static readonly ErrorSmartEnum Weak_Password = new ErrorSmartEnum("Password is too weak.", 7); public static readonly ErrorSmartEnum Wrong_Captcha = new ErrorSmartEnum("Wrong Captcha.", 13); public static readonly ErrorSmartEnum Not_Authorized = new ErrorSmartEnum("Not Authorized.", 21); public static readonly ErrorSmartEnum Mandatory_Fields = new ErrorSmartEnum("Fields Marked With * are mandatory.", 25); } }
Bear in mind you won’t be able to use any of the Enum class methods, however, the SmartEnum
class offers similar methods out-of-the-box, e.g. a List Property that returns all constants in a read-only fashion and it’s conveniently available within your defined enum
.
5.5.1 SmartEnum Methods and Properties
Returns a read-only collection containing all defined constants.
private static void PrintSmartEnum() { foreach (var error in ErrorSmartEnum.List) { Console.WriteLine("Error code number {0}, message: {1}", error.Value, error); // error is the same as error.Name } }
Output:
Error code number 25, message: Fields Marked With * are mandatory. Error code number 1, message: Invalid ID. Error code number 21, message: Not Authorized. Error code number 7, message: Password is too weak. Error code number 13, message: Wrong Captcha.
Besides the List
property, it offers some useful methods that by now you should already be familiar with, and the only difference is that they have better naming, and you can use them directly from your SmartEnum
class instead of calling the Enum
class to do so and since they are virtually identical, we won’t go through every detail all over again.
private static void TryFromValue(int value) { if (ErrorSmartEnum.TryFromValue(value, out ErrorSmartEnum error)) { Console.WriteLine("Successfully parsed: {0}", error); } else { Console.WriteLine("Not an enum value!"); } } private static void TryFromName(string name) { if (ErrorSmartEnum.TryFromName(name, out ErrorSmartEnum error)) { Console.WriteLine("Successfully parsed: {0}", error); } else { Console.WriteLine("Not an enum name!"); } } private static void FromValue(int value) { // Will throw an Exception if not found. Console.WriteLine("From value: {0}", ErrorSmartEnum.FromValue(value)); } private static void FromName(string name) { // Will throw an Exception if not found. Console.WriteLine("From name: {0}", ErrorSmartEnum.FromName(name)); }
Main method call:
TryFromValue(13); TryFromValue(15); TryFromName("Invalid ID."); TryFromName("Invalid Password."); FromValue(13); //FromValue(15); // UNCOMMENT to Throw ThrowValueNotFoundException FromName("Invalid ID."); //FromName("Invalid Password."); // UNCOMMENT to Throw ThrowNameNotFoundException
Output:
Successfully parsed: Wrong Captcha. Not an enum value! Successfully parsed: Invalid ID. Not an enum name! From value: Wrong Captcha. From name: Invalid ID.
6. Conclusion
By now you should have a great understanding of how Enum works in C#. How to make the most of it and how professionals circumvent the Enum limitation by writing smarter Enumerations using all the approaches mentioned above. Finally, you can find the source code on our GitHub page.
7. Sources
[1]: Enum Class
[2]: Enumeration types
[3]: GetCustomAttributes Method
[4]: Bitwise operation