Chuyển tới nội dung chính

Enums

Definition

An enum (enumeration) is a value type that defines a set of named constants. Under the hood, each enum member maps to an integral numeric value, making them type-safe and readable alternatives to "magic numbers."

Core Concepts

Basic Declaration and Usage

public enum Season
{
Spring,
Summer,
Autumn,
Winter
}

Season current = Season.Summer;
Console.WriteLine(current); // "Summer"
Console.WriteLine((int)current); // 1 (zero-based by default)

Underlying Type

By default, enums use int. You can specify any integral type: byte, sbyte, short, ushort, int, uint, long, ulong.

public enum HttpStatus : ushort
{
OK = 200,
NotFound = 404,
InternalServerError = 500
}

Custom Values

You can assign explicit values. Unassigned members continue from the previous value + 1.

public enum HttpStatusCode
{
OK = 200,
Created = 201,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404
}

Code Examples

Enum Methods

public enum Color { Red, Green, Blue }

// Get name from value
string name = Enum.GetName(typeof(Color), 1); // "Green"
string name2 = Enum.GetName<Color>(2); // "Blue" (generic overload)

// Get all names
string[] names = Enum.GetNames<Color>(); // ["Red", "Green", "Blue"]

// Get all values
Color[] values = Enum.GetValues<Color>(); // [Red, Green, Blue]

// Parse from string
Color parsed = Enum.Parse<Color>("Green"); // Color.Green

// Try parse (safe)
if (Enum.TryParse<Color>("RED", ignoreCase: true, out var result))
Console.WriteLine(result); // Color.Red

// Check if a value is defined
bool exists = Enum.IsDefined(typeof(Color), 5); // false
bool exists2 = Enum.IsDefined<Color>(Color.Red); // true

// Get underlying numeric value
int numeric = Convert.ToInt32(Color.Blue); // 2

Casting Between Enum and Underlying Type

Color color = Color.Green;
int value = (int)color; // 1
Color fromInt = (Color)1; // Color.Green

// Be careful — invalid casts compile but produce undefined enum values
Color invalid = (Color)99; // compiles! IsDefined returns false
Casting Invalid Values

C# allows casting any integral value to an enum, even if no member has that value. Always validate with Enum.IsDefined when receiving enum values from external sources (APIs, databases, user input).

The [Flags] Attribute

The [Flags] attribute enables a single enum variable to hold a combination of values using bitwise operations.

[Flags]
public enum Permissions
{
None = 0b_0000, // 0
Read = 0b_0001, // 1
Write = 0b_0010, // 2
Execute = 0b_0100, // 4
All = Read | Write | Execute // 7
}

// Combining flags
Permissions myPerms = Permissions.Read | Permissions.Write;
Console.WriteLine(myPerms); // "Read, Write"

// Checking with HasFlag
bool canRead = myPerms.HasFlag(Permissions.Read); // true
bool canExec = myPerms.HasFlag(Permissions.Execute); // false

// Bitwise check (equivalent, slightly faster)
bool canRead2 = (myPerms & Permissions.Read) == Permissions.Read; // true

Switch with Enums

public string GetSeasonMessage(Season season) => season switch
{
Season.Spring => "Flowers are blooming",
Season.Summer => "Time for the beach",
Season.Autumn => "Leaves are falling",
Season.Winter => "Bundle up!",
_ => "Unknown season" // handles future/invalid values
};
Exhaustive Matching

When switching on an enum, the compiler warns if you miss a member. However, always include a _ (discard) or default case because a cast like (Season)99 can bypass enum constraints at runtime.

Enum vs Constant Class vs Smart Enum

// Approach 1: Enum — simple, fast, but no behavior
public enum OrderStatus { Pending, Shipped, Delivered, Cancelled }

// Approach 2: Constant class — groups constants but no type safety
public static class OrderStatuses
{
public const string Pending = "Pending";
public const string Shipped = "Shipped";
}

// Approach 3: Smart Enum pattern — encapsulates behavior with each value
public abstract class SmartOrderStatus : Enumeration
{
public static readonly SmartOrderStatus Pending = new PendingStatus();
public static readonly SmartOrderStatus Shipped = new ShippedStatus();

public abstract bool CanTransitionTo(SmartOrderStatus next);

private class PendingStatus : SmartOrderStatus
{
public override bool CanTransitionTo(SmartOrderStatus next) =>
next == Shipped || next == Cancelled;
}

private class ShippedStatus : SmartOrderStatus
{
public override bool CanTransitionTo(SmartOrderStatus next) =>
next == Delivered;
}
}

When to Use Enums

ApproachWhen to Use
EnumFixed set of named constants, no behavior needed, fast comparisons
Constant classString values, backward-compatible APIs, serialization scenarios
Smart enumEach value needs associated behavior or validation rules

Best Practices

  • Use singular names for regular enums (Color, Season).
  • Use plural names for [Flags] enums (Permissions, Options).
  • For [Flags] enums, always include a None = 0 member.
  • Assign powers of 2 (1, 2, 4, 8...) to [Flags] members.
  • Use Enum.IsDefined to validate enum values from external input.
  • Avoid using enums in public API boundaries across services — prefer strings or integers for serialization, as enum values can change between versions.
[Flags]
public enum FileAttributes
{
None = 0,
ReadOnly = 1 << 0, // 1
Hidden = 1 << 1, // 2
System = 1 << 2, // 4
Archive = 1 << 3, // 8
Compressed = 1 << 4 // 16
}

Common Pitfalls

  • Casting invalid integers(MyEnum)999 compiles but produces a value with no defined name. Validate with Enum.IsDefined.
  • Enum.IsDefined performance — uses reflection internally. For hot paths, consider a HashSet<T> or switch-based validation.
  • Enums in public APIs — if you add or rename a member, it can break serialization. Consider using integers or strings at service boundaries and mapping to internal enums.
  • Zero value ambiguity — the default value of any enum is 0. If no member is assigned 0, a default-initialized variable holds an unnamed value. Always explicitly define a None = 0 or a meaningful zero member.
public enum Status
{
Active = 1, // no zero value defined
Inactive = 2
}

Status s = default; // s == 0, which is NOT Active or Inactive — potential bug

Key Takeaways

  • Enums map named constants to integral types, providing type safety and readability.
  • Use [Flags] for bitwise combinations and always use powers of 2.
  • Always validate enum values from external sources with Enum.IsDefined.
  • Prefer singular names for regular enums, plural for flags.
  • Consider the smart enum pattern when enum values need associated behavior.

Interview Questions

Q: What is an enum? A: An enum is a value type that defines a set of named integral constants. It improves code readability and type safety by replacing magic numbers with meaningful names.

Q: How do [Flags] enums work? A: The [Flags] attribute indicates that the enum represents a set of bitwise fields. Each member is assigned a power of 2 so they can be combined with | (OR) and tested with HasFlag or & (AND). The ToString() output shows all matching names as a comma-separated list.

Q: What is the default value of an enum? A: The default value is always 0 (the zero value of the underlying integral type). If no member is explicitly assigned 0, the default will be an unnamed value. Best practice is to always define a None = 0 member.

Q: Can you extend an enum? A: No. Enums are sealed and cannot be inherited or extended at compile time. You can add new values to the enum definition, but this is a breaking change for consumers. For extensible behavior, use the smart enum pattern or a strategy-based approach.

Q: What is the difference between Enum.Parse and Enum.TryParse? A: Enum.Parse throws an exception if the string does not match any member. Enum.TryParse returns a boolean indicating success and outputs the result via an out parameter — it is the safer choice for runtime input.

References