Enums
|
This section documents C# 14 on .NET 10 (LTS), as published at learn.microsoft.com/dotnet/csharp, which is the reference these pages are written and verified against. Features introduced by C# 15 / .NET 11 are still in preview and are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against learn.microsoft.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
An enum is a value type whose values are named constants over an integral type. It gives a fixed set of
options a name, a type and IntelliSense — everything a bare int or string does not.
Declaring an Enum
public enum LogLevel
{
Trace, // 0 -- values start at zero and increment
Debug, // 1
Information,// 2
Warning, // 3
Error, // 4
Critical, // 5
}
public static class EnumBasics
{
public static void Run()
{
LogLevel level = LogLevel.Warning;
Console.WriteLine(level); // Warning -- ToString gives the name
Console.WriteLine((int)level); // 3
Console.WriteLine(level > LogLevel.Information); // True -- comparison works
}
}
The underlying type is int unless you say otherwise:
// A narrower underlying type -- useful for interop and dense storage.
public enum Status : byte
{
Unknown = 0,
Active = 1,
Suspended = 2,
Closed = 255,
}
// Values may be explicit, repeated (aliases) and out of order.
public enum HttpStatus
{
Ok = 200,
Created = 201,
NoContent = 204,
MovedPermanently = 301,
BadRequest = 400,
NotFound = 404,
Gone = 410,
ServerError = 500,
Success = Ok, // an alias for the same value
}
Permitted underlying types are byte, sbyte, short, ushort, int, uint, long and ulong.
|
An enum variable is not restricted to its declared members: |
[Flags] Enums
When the values are independent bits that combine, mark the enum [Flags] and give each member a distinct
power of two:
[Flags]
public enum FileAccess2
{
None = 0,
Read = 1 << 0, // 1
Write = 1 << 1, // 2
Execute = 1 << 2, // 4
Delete = 1 << 3, // 8
ReadWrite = Read | Write, // a named combination
All = Read | Write | Execute | Delete, // 15
}
public static class FlagsDemo
{
public static void Run()
{
FileAccess2 access = FileAccess2.Read | FileAccess2.Write;
Console.WriteLine(access); // Read, Write -- [Flags] ToString
Console.WriteLine((int)access); // 3
// Testing a flag -- the fast, allocation-free form.
Console.WriteLine((access & FileAccess2.Write) != 0); // True
// HasFlag reads better; it boxes on older runtimes but is optimised now.
Console.WriteLine(access.HasFlag(FileAccess2.Write)); // True
Console.WriteLine(access.HasFlag(FileAccess2.ReadWrite)); // True -- ALL bits must be set
// Adding and removing flags.
access |= FileAccess2.Execute; // add
access &= ~FileAccess2.Read; // remove
access ^= FileAccess2.Delete; // toggle
Console.WriteLine(access);
Console.WriteLine(FileAccess2.None == 0); // True
}
}
Always include a zero member named None — it is the value of a default variable, and HasFlag(None) is
always true, which is rarely what the caller means.
Converting
public static class EnumConversion
{
public static void Run()
{
// enum <-> integer: explicit casts, both directions.
LogLevel level = (LogLevel)4;
int number = (int)LogLevel.Error;
Console.WriteLine($"{level} {number}");
// enum -> string
Console.WriteLine(LogLevel.Warning.ToString()); // Warning
Console.WriteLine($"{LogLevel.Warning:D}"); // 3 -- decimal format
Console.WriteLine($"{LogLevel.Warning:G}"); // Warning
Console.WriteLine($"{FileAccess2.ReadWrite:F}"); // Read, Write -- flags format
// string -> enum: prefer TryParse for untrusted input.
if (Enum.TryParse("Warning", ignoreCase: true, out LogLevel parsed))
{
Console.WriteLine(parsed);
}
// Parse throws on failure; both accept numeric text too, which is a common trap:
Console.WriteLine(Enum.Parse<LogLevel>("3")); // Warning -- from "3"!
Console.WriteLine(Enum.TryParse("99", out LogLevel outOfRange)); // True (!)
Console.WriteLine(Enum.IsDefined(outOfRange)); // False -- the real check
// Enumerating members.
foreach (LogLevel value in Enum.GetValues<LogLevel>())
{
Console.Write($"{value}={(int)value} ");
}
Console.WriteLine();
Console.WriteLine(string.Join(",", Enum.GetNames<LogLevel>()));
}
}
Enum.GetValues<T>() and Enum.GetNames<T>() are the generic (allocation-friendlier) overloads; the
non-generic Enum.GetValues(typeof(T)) returns an Array you must cast.
Switching over Enums
public static class EnumSwitching
{
public static ConsoleColor ColourFor(LogLevel level) => level switch
{
LogLevel.Trace or LogLevel.Debug => ConsoleColor.Gray,
LogLevel.Information => ConsoleColor.White,
LogLevel.Warning => ConsoleColor.Yellow,
LogLevel.Error or LogLevel.Critical => ConsoleColor.Red,
_ => throw new ArgumentOutOfRangeException(nameof(level), level, "Unknown log level."),
};
// Relational patterns work because enums are ordered.
public static bool ShouldAlert(LogLevel level) => level switch
{
>= LogLevel.Error => true,
_ => false,
};
}
The compiler warns (CS8509) if a switch expression over an enum omits a declared member, which makes adding a
member a compile-time prompt to handle it. Keep the _ arm anyway — it catches the out-of-range casts above.
For a [Flags] enum, a switch on the whole value is usually wrong; test the bits instead.
Enums in Generics
public static class EnumHelpers
{
// `struct, Enum` is the constraint that means "any enum type".
public static IReadOnlyList<T> AllValuesOf<T>() where T : struct, Enum
=> Enum.GetValues<T>();
public static bool IsValid<T>(T value) where T : struct, Enum
=> Enum.IsDefined(value);
public static T ParseOrDefault<T>(string text, T fallback) where T : struct, Enum
=> Enum.TryParse(text, ignoreCase: true, out T parsed) && Enum.IsDefined(parsed)
? parsed
: fallback;
public static void Demo()
{
Console.WriteLine(AllValuesOf<LogLevel>().Count); // 6
Console.WriteLine(IsValid((LogLevel)99)); // False
Console.WriteLine(ParseOrDefault("critical", LogLevel.Information));
}
}
Best Practices
-
Name the type in the singular (
LogLevel,Status) — except a[Flags]enum, which takes a plural (FileAccess,RegexOptions). -
Do not prefix members with the type name:
LogLevel.Error, notLogLevel.LogLevelError. -
Give the zero value a meaning.
default(T)is zero, so member zero should beNone,Unknownor the sensible default — never an arbitrary real option. -
Pin the numeric values on anything persisted or sent over a wire. If the values are stored in a database or serialised, reordering members silently changes their meaning; assign explicit numbers and never reuse one.
-
Avoid
[Flags]unless the values genuinely combine. If only one option can apply at a time, a plain enum is clearer and switchable. -
Prefer
intas the underlying type unless interop or storage density demands otherwise — narrower types save nothing in a local and complicate arithmetic. -
Validate at the boundary. Cast an
intfrom a database or a request into an enum and you have an unvalidated value; run it throughEnum.IsDefined(or aswitchthat throws) first. -
When behaviour, not just a label, differs per value, consider a
sealed recordhierarchy or a dictionary of delegates instead — an enum plus a growingswitchin five places is a sign the type wants to be polymorphic.
See Also
-
Basic Types and Variables — the integral types enums are built on, and conversions.
-
Operators and Expressions — the bitwise and shift operators
[Flags]enums rely on. -
Pattern Matching — constant and relational patterns over enums.
-
Generics — the
where T : struct, Enumconstraint.