C# Enum Declaration: Syntax, Types, and Pitfalls
c# enum declaration: Understand C# enum declaration syntax, underlying types, flags, naming, and versioning to write maintainable and efficient code.
The c# enum declaration syntax is simple, but the choices you make when declaring an enum affect type safety, storage, and how the enum behaves across versions. A basic enum declaration looks like this:
public enum Color { Red, Green, Blue }
Each member gets an underlying integer value starting at 0 by default. You can assign explicit values to control the numeric representation, which is useful when an enum maps to external data or a storage format.
Declaring a Basic Enum in C#
The enum keyword defines a distinct type whose named constants are known as enum members. The default underlying type is int, and the first member has value 0 unless you specify otherwise. You can place an enum at namespace level or inside a class, but not inside a method.
public enum StatusCode { Ok = 200, NotFound = 404, ServerError = 500 }
Explicit values allow the enum to match an external contract, such as an HTTP status code or a database column. When you omit values, the compiler assigns consecutive integers starting from 0, which can be convenient for internal flags or simple state machines.
Choosing the Underlying Type
By default, an enum uses int, but you can declare any integral type except char. Common choices include byte, short, long, and their unsigned counterparts. The underlying type affects memory usage and interop behavior.
public enum Permission : byte { Read = 1, Write = 2, Execute = 4 }
Use a smaller underlying type when you need to save memory in large arrays or when you are interoperating with a C or C++ struct that defines a specific size. For most application code, the default int is the right choice because it avoids unnecessary casting and matches the runtime's natural word size.
Using the [Flags] Attribute for Bitwise Combinations
When an enum represents a set of options that can be combined, apply the [Flags] attribute and assign powers of two. This enables bitwise operations such as | and &.
[Flags] public enum FileAccess { None = 0, Read = 1, Write = 2, Execute = 4 }
The [Flags] attribute changes how ToString() formats the value: a combination like Read | Write becomes "Read, Write" instead of a single integer. Without the attribute, bitwise operations still work, but the behavior is less discoverable and the string representation is misleading. Use [Flags] only when the enum is designed for bitwise composition.
Naming Conventions That Keep Enums Readable
The .NET naming guidelines recommend a singular name for most enums, unless the enum itself represents a collection of bit flags. For example, Color is singular, while FileAccess with [Flags] is also singular because it represents a set of flags. Member names should be PascalCase and should not repeat the enum name. Color.Red is clearer than Color.ColorRed.
public enum Priority { Low, Medium, High }
Avoid using an enum when the set of values is expected to change frequently. Adding a new member to an enum is a breaking change for consumers that use switch statements without a default case. Consider a class with static readonly fields if extensibility is a primary requirement.
Common Pitfalls in Enum Declaration and Usage
One common mistake is relying on the default value of an enum. The default value of any enum is 0, even if no member is defined with that value. This can cause subtle bugs when a struct is initialized without an explicit assignment.
public enum Level { Low = 1, Medium = 2, High = 3 } Level level = default; // This is 0, not Low
Another issue is duplicate values. You can assign the same numeric value to multiple members, which makes Enum.ToString() ambiguous. The runtime returns the first matching name, which may not be the one you expect. Avoid duplicate values unless you have a specific interop reason.
Parsing user input with Enum.Parse is case-sensitive by default and throws an exception for invalid strings. Use Enum.TryParse with ignoreCase: true to handle user input gracefully.
if (Enum.TryParse<Level>("medium", true, out var level)) { // level is Level.Medium }
Runtime Behavior and Performance of Enums
Enums are value types stored inline in their containing structure. An enum variable does not allocate on the heap unless it is boxed. Boxing occurs when you pass an enum to a method that expects object, such as Console.WriteLine or string.Format. In hot paths, avoid boxing by calling ToString() explicitly or by using generic methods.
Enum comparisons and switches compile to efficient integer comparisons. The runtime treats an enum as its underlying type for arithmetic, so there is no overhead beyond the underlying type's cost. When you need to iterate over all values, Enum.GetValues allocates an array and boxes each value, so it is not suitable for performance-critical loops.
Versioning and Compatibility Concerns
Changing an enum's underlying type or adding members can break binary compatibility. If you ship a library, consider the public API impact. Adding a member forces downstream consumers to handle the new value in their switch statements, especially if they use default to throw. Removing a member breaks code that references it.
If you must extend an enum, document the values that are reserved and consider using explicit values to avoid shifting existing members. Changing the underlying type from int to long changes the size of the type and can break interop or serialization contracts.
For flags enums, always reserve a None value of 0 and keep powers of two for all other members. This preserves backward compatibility when new flags are added, as long as the numeric values remain stable.