Easy Learn C#

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 integer
  • long: 64-bit signed integer
  • ulong: 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 of DaysOfWeek
  • 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
    }
}