Enums in C#
Introduction to Enums
An enum (short for enumeration) is a value type in C# that represents a set of named constants. Enums make code more readable, maintainable, and less error-prone by replacing magic numbers or strings with descriptive names.
Why Use Enums?
- Create meaningful names for numeric values
- Group related constants together
- Make code more readable and self-documenting
- Provide type safety for a set of related values
- Enable IDE intellisense for available options
- Prevent invalid values through compile-time checking
Basic Enum Syntax
Enums are defined using the enum
keyword. By default, enum constants are assigned integer values starting from 0.
Declaring and Using Enums
// Basic enum declaration
public enum DayOfWeek
{
Sunday, // 0
Monday, // 1
Tuesday, // 2
Wednesday, // 3
Thursday, // 4
Friday, // 5
Saturday // 6
}
// Enum with explicit values
public enum HttpStatusCode
{
OK = 200,
Created = 201,
Accepted = 202,
NoContent = 204,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
InternalServerError = 500
}
// Using enums
public class Program
{
public static void Main()
{
// Declaring enum variables
DayOfWeek today = DayOfWeek.Wednesday;
HttpStatusCode response = HttpStatusCode.OK;
// Using in conditions
if (today == DayOfWeek.Saturday || today == DayOfWeek.Sunday)
{
Console.WriteLine("It's the weekend!");
}
else
{
Console.WriteLine("It's a weekday.");
}
// Using in switch statements
switch (response)
{
case HttpStatusCode.OK:
Console.WriteLine("Request succeeded");
break;
case HttpStatusCode.NotFound:
Console.WriteLine("Resource not found");
break;
case HttpStatusCode.InternalServerError:
Console.WriteLine("Server error occurred");
break;
default:
Console.WriteLine($"Status code: {response}");
break;
}
}
}
Key points about basic enum usage:
- Enum members are accessed using dot notation (e.g.,
DayOfWeek.Monday
) - By default, the first enum value is 0, and each subsequent value increases by 1
- You can assign explicit values to enum members
- Enum values don't need to be unique or sequential
- Enums make code more readable than using raw numbers
Enum Underlying Types
By default, the underlying type of an enum is int
, but you can specify a different integral type using a colon and the type name.
Specifying Underlying Types
// Default (int) enum
public enum Season
{
Spring,
Summer,
Autumn,
Winter
}
// Byte enum (saves memory if you have many instances)
public enum FilePermission : byte
{
None = 0,
Read = 1,
Write = 2,
Execute = 4
}
// Long enum (for larger values)
public enum LargeValue : long
{
Max = 9223372036854775807 // Maximum value for long
}
// Short enum
public enum SmallEnum : short
{
A = 1,
B = 2,
C = 3
}
// Unsigned int enum
public enum UnsignedEnum : uint
{
Value1 = 1,
Value2 = 2,
LargeValue = 4294967295 // Maximum value for uint
}
Supported underlying types for enums:
byte
: 8-bit unsigned integer (0 to 255)sbyte
: 8-bit signed integer (-128 to 127)short
: 16-bit signed integer (-32,768 to 32,767)ushort
: 16-bit unsigned integer (0 to 65,535)int
: 32-bit signed integer (default)uint
: 32-bit unsigned integerlong
: 64-bit signed integerulong
: 64-bit unsigned integer
Choose an underlying type based on:
- The range of values needed
- Memory efficiency considerations
- Interoperability requirements
Working with Enum Values
C# provides several ways to work with enum values, including conversions, parsing, and getting all possible values.
Converting Between Enums and Numeric Types
public enum Color
{
Red = 1,
Green = 2,
Blue = 3
}
public class Program
{
public static void Main()
{
// Converting enum to integer
Color color = Color.Green;
int colorValue = (int)color; // Explicit cast
Console.WriteLine($"{color} has value {colorValue}"); // "Green has value 2"
// Converting integer to enum
int value = 3;
Color parsedColor = (Color)value; // Explicit cast
Console.WriteLine($"Value {value} is color {parsedColor}"); // "Value 3 is color Blue"
// Be careful with out of range values
int invalidValue = 99;
Color unknownColor = (Color)invalidValue;
Console.WriteLine($"Value {invalidValue} is color {unknownColor}"); // "Value 99 is color 99"
// Checking if a value is defined in the enum
bool isValidColor = Enum.IsDefined(typeof(Color), unknownColor);
Console.WriteLine($"Is {unknownColor} a valid color? {isValidColor}"); // "Is 99 a valid color? False"
// Parsing enum from string
string colorName = "Red";
if (Enum.TryParse(colorName, out Color redColor))
{
Console.WriteLine($"Parsed {colorName} to {redColor}"); // "Parsed Red to Red"
}
// Case-insensitive parsing
string lowerColorName = "blue";
if (Enum.TryParse(lowerColorName, true, out Color blueColor))
{
Console.WriteLine($"Parsed {lowerColorName} to {blueColor}"); // "Parsed blue to Blue"
}
// Getting all values of an enum
Console.WriteLine("All colors:");
foreach (Color c in Enum.GetValues(typeof(Color)))
{
Console.WriteLine($"- {c} ({(int)c})");
}
// Getting all names of an enum
Console.WriteLine("All color names:");
foreach (string name in Enum.GetNames(typeof(Color)))
{
Console.WriteLine($"- {name}");
}
}
}
Working with enum values:
- Use explicit casting to convert between enums and their underlying types
- Use
Enum.IsDefined()
to check if a value is defined in an enum - Use
Enum.TryParse()
to safely convert strings to enum values - Use
Enum.GetValues()
to get all values of an enum - Use
Enum.GetNames()
to get all names of an enum
Flag Enums
Flag enums allow multiple enum values to be combined using bitwise operations. They're particularly useful for representing a set of options or flags.
Creating and Using Flag Enums
// Flag enum - use [Flags] attribute and powers of 2 for values
[Flags]
public enum FilePermission
{
None = 0, // 0000
Read = 1, // 0001
Write = 2, // 0010
Execute = 4, // 0100
Delete = 8, // 1000
ReadWrite = Read | Write, // 0011 = 3
ReadExecute = Read | Execute, // 0101 = 5
ReadWriteExecute = Read | Write | Execute, // 0111 = 7
All = Read | Write | Execute | Delete // 1111 = 15
}
public class Program
{
public static void Main()
{
// Creating a combination of flags
FilePermission permissions = FilePermission.Read | FilePermission.Write;
Console.WriteLine(permissions); // "ReadWrite" or "Read, Write" depending on framework version
// Alternative way to create the same combination
permissions = FilePermission.ReadWrite;
// Checking if a specific flag is set
bool canRead = (permissions & FilePermission.Read) == FilePermission.Read;
bool canExecute = (permissions & FilePermission.Execute) == FilePermission.Execute;
Console.WriteLine($"Can read: {canRead}"); // "Can read: True"
Console.WriteLine($"Can execute: {canExecute}"); // "Can execute: False"
// Checking if any flag is set
bool hasAnyPermission = permissions != FilePermission.None;
// Adding a flag
permissions |= FilePermission.Execute;
Console.WriteLine(permissions); // "ReadWriteExecute" or "Read, Write, Execute"
// Removing a flag
permissions &= ~FilePermission.Write;
Console.WriteLine(permissions); // "ReadExecute" or "Read, Execute"
// Toggling a flag
permissions ^= FilePermission.Execute;
Console.WriteLine(permissions); // "Read"
// Using HasFlag method (simpler but less performant)
FilePermission allPermissions = FilePermission.All;
bool hasReadPermission = allPermissions.HasFlag(FilePermission.Read);
Console.WriteLine($"Has read permission: {hasReadPermission}"); // "Has read permission: True"
}
}
Key points about flag enums:
- Use the
[Flags]
attribute to indicate a flag enum - Use powers of 2 (1, 2, 4, 8, 16, etc.) for individual flags
- Use bitwise OR (|) to combine flags
- Use bitwise AND (&) to check if a flag is set
- Use bitwise NOT (~) with AND (&) to remove a flag
- Use bitwise XOR (^) to toggle a flag
- The
[Flags]
attribute enables a better string representation when multiple flags are combined
Enum Extensions and Helpers
You can extend enums with extension methods to add functionality such as description attributes, validation, or conversion methods.
Adding Description Attributes
using System.ComponentModel;
using System.Reflection;
// Enum with descriptions
public enum PaymentMethod
{
[Description("Credit Card Payment")]
CreditCard,
[Description("PayPal Payment")]
PayPal,
[Description("Bank Transfer")]
BankTransfer,
[Description("Cash on Delivery")]
CashOnDelivery
}
// Extension method to get description
public static class EnumExtensions
{
public static string GetDescription(this Enum value)
{
Type type = value.GetType();
string name = Enum.GetName(type, value);
if (name != null)
{
FieldInfo field = type.GetField(name);
if (field != null)
{
if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute)
{
return attribute.Description;
}
}
}
return value.ToString();
}
// Extension method for flags enum to get individual flags
public static IEnumerable GetFlags(this T flags) where T : Enum
{
foreach (Enum value in Enum.GetValues(typeof(T)))
{
if (flags.HasFlag(value) && !value.Equals(default(T)))
{
yield return (T)value;
}
}
}
}
// Usage
public class Program
{
public static void Main()
{
// Using the description extension
PaymentMethod payment = PaymentMethod.CreditCard;
string description = payment.GetDescription();
Console.WriteLine(description); // "Credit Card Payment"
// Using flag enum extension
[Flags]
enum Permissions { None = 0, Read = 1, Write = 2, Delete = 4, All = Read | Write | Delete }
Permissions userPermissions = Permissions.Read | Permissions.Write;
Console.WriteLine("User has these permissions:");
foreach (var permission in userPermissions.GetFlags())
{
Console.WriteLine($"- {permission}");
}
// Outputs:
// User has these permissions:
// - Read
// - Write
}
}
Enum Best Practices
Guidelines for Using Enums
- Use PascalCase for enum names and values - Follow C# naming conventions
- Use singular names for enums - For example,
DayOfWeek
instead ofDaysOfWeek
- Provide a "None" or "Default" value - Often with a value of 0
- Avoid using enum values as bit flags unless appropriate - Only use [Flags] when you need combinations
- Consider using explicit values - Especially when serializing or storing in a database
- Don't change enum values once published - This can break serialization or stored data
- Consider adding a description attribute - For UI display or documentation
- Check for undefined values - Use
Enum.IsDefined
when working with user input - Use switch statements with a default case - To handle all enum values
- Consider extension methods - For common operations on your enums
Common Enum Mistakes to Avoid
- Using non-sequential values unnecessarily - Makes code harder to understand
- Creating enums with too many values - Consider breaking into multiple enums
- Changing enum values after deployment - Can break existing data
- Using raw integers instead of named constants - Defeats the purpose of enums
- Not handling undefined enum values - Can cause unexpected behavior
- Misusing flag enums - Don't use [Flags] if you don't need combinations
- Not documenting enum values - Makes it harder for other developers to understand
Real-World Examples
State Machine with Enums
// Order status state machine
public enum OrderStatus
{
Created,
PaymentPending,
PaymentReceived,
Preparing,
Shipped,
Delivered,
Canceled,
Returned
}
public class Order
{
public int OrderId { get; set; }
public OrderStatus Status { get; private set; }
public DateTime LastUpdated { get; private set; }
public Order(int orderId)
{
OrderId = orderId;
Status = OrderStatus.Created;
LastUpdated = DateTime.Now;
}
public bool UpdateStatus(OrderStatus newStatus)
{
// Validate the status transition
if (!IsValidStatusTransition(Status, newStatus))
{
return false;
}
Status = newStatus;
LastUpdated = DateTime.Now;
// Perform additional actions based on new status
switch (newStatus)
{
case OrderStatus.PaymentReceived:
SendPaymentConfirmation();
break;
case OrderStatus.Shipped:
SendShippingNotification();
break;
case OrderStatus.Canceled:
ProcessRefund();
break;
}
return true;
}
private bool IsValidStatusTransition(OrderStatus currentStatus, OrderStatus newStatus)
{
switch (currentStatus)
{
case OrderStatus.Created:
return newStatus == OrderStatus.PaymentPending || newStatus == OrderStatus.Canceled;
case OrderStatus.PaymentPending:
return newStatus == OrderStatus.PaymentReceived || newStatus == OrderStatus.Canceled;
case OrderStatus.PaymentReceived:
return newStatus == OrderStatus.Preparing || newStatus == OrderStatus.Canceled;
case OrderStatus.Preparing:
return newStatus == OrderStatus.Shipped || newStatus == OrderStatus.Canceled;
case OrderStatus.Shipped:
return newStatus == OrderStatus.Delivered || newStatus == OrderStatus.Canceled;
case OrderStatus.Delivered:
return newStatus == OrderStatus.Returned;
default:
return false; // Canceled or Returned are final states
}
}
private void SendPaymentConfirmation()
{
// Implementation
}
private void SendShippingNotification()
{
// Implementation
}
private void ProcessRefund()
{
// Implementation
}
}
Configuration Options with Flag Enums
[Flags]
public enum LoggingOptions
{
None = 0,
LogToConsole = 1,
LogToFile = 2,
LogToDatabase = 4,
LogErrors = 8,
LogWarnings = 16,
LogInfo = 32,
LogDebug = 64,
// Common combinations
LogAllLevels = LogErrors | LogWarnings | LogInfo | LogDebug,
LogErrorsAndWarnings = LogErrors | LogWarnings,
LogToAll = LogToConsole | LogToFile | LogToDatabase,
// Default configuration
DefaultConfig = LogToConsole | LogToFile | LogErrors | LogWarnings | LogInfo
}
public class Logger
{
private LoggingOptions _options;
public Logger(LoggingOptions options = LoggingOptions.DefaultConfig)
{
_options = options;
}
public void LogMessage(string message, LoggingOptions level)
{
// Check if this level should be logged
if ((_options & level) == 0)
{
return; // This level is not enabled
}
string formattedMessage = $"[{DateTime.Now}] [{level}] {message}";
// Determine where to log the message
if (_options.HasFlag(LoggingOptions.LogToConsole))
{
Console.WriteLine(formattedMessage);
}
if (_options.HasFlag(LoggingOptions.LogToFile))
{
// Implementation for file logging
}
if (_options.HasFlag(LoggingOptions.LogToDatabase))
{
// Implementation for database logging
}
}
public void Error(string message) => LogMessage(message, LoggingOptions.LogErrors);
public void Warning(string message) => LogMessage(message, LoggingOptions.LogWarnings);
public void Info(string message) => LogMessage(message, LoggingOptions.LogInfo);
public void Debug(string message) => LogMessage(message, LoggingOptions.LogDebug);
}
// Usage
public class Program
{
public static void Main()
{
// Default logger
Logger defaultLogger = new Logger();
// Custom logger that only logs errors to the console
Logger errorLogger = new Logger(LoggingOptions.LogToConsole | LoggingOptions.LogErrors);
// Verbose logger for debugging
Logger debugLogger = new Logger(LoggingOptions.LogToAll | LoggingOptions.LogAllLevels);
// Example usage
defaultLogger.Error("This is an error"); // Will be logged
defaultLogger.Warning("This is a warning"); // Will be logged
defaultLogger.Info("This is info"); // Will be logged
defaultLogger.Debug("This is debug"); // Won't be logged (not in default config)
errorLogger.Error("Critical error"); // Will be logged
errorLogger.Warning("Minor warning"); // Won't be logged
}
}