Enums

A named set of fixed values with an integer behind each name, how to convert both ways, and why it beats loose string constants.

Syntaxenum OrderStatus { Pending = 0, Shipped = 1, Delivered = 2 }

An enum is a type with a fixed set of named values. An order is Pending, Shipped or Delivered, and nothing else. Writing that as an enum means the compiler rejects any other name, so a typo becomes a build error instead of a bug found in production.

Behind each name is a whole number, called the underlying value. By default the first member is 0, the next 1, and so on. You can set the numbers yourself, and you should whenever the values are stored in a database or sent over a network, because then nobody can change them by reordering the members.

Converting both ways

A cast turns an enum into its number and back: (int)status gives 1, and (OrderStatus)1 gives OrderStatus.Shipped. status.ToString() gives the name. Enum.TryParse<OrderStatus>(text, out var value) turns text into a value and returns false instead of failing when the text does not match any member.

What trips people up

An enum is not a closed set at run time. A cast from any integer succeeds, even one that no member uses, so a value read from a database or a web request can be a number with no name attached. Enum.IsDefined(value) reports whether some member really has that value. Prefer an enum to a handful of string constants: strings compare case-sensitively, can be misspelled, and give you no help at all from the compiler.

Example

Example · csharp
OrderStatus status = OrderStatus.Shipped;

Console.WriteLine(status);            // the name
Console.WriteLine((int)status);       // the number behind the name

// Text from outside the program, turned into an enum value.
// ignoreCase: true accepts "delivered" as well as "Delivered".
if (Enum.TryParse<OrderStatus>("delivered", ignoreCase: true, out OrderStatus parsed))
{
    Console.WriteLine($"parsed: {parsed}");
}

// A number that no member uses still converts, so check it.
OrderStatus odd = (OrderStatus)99;
Console.WriteLine(Enum.IsDefined(odd));

enum OrderStatus
{
    Pending = 0,
    Shipped = 1,
    Delivered = 2
}

// Output:
// Shipped
// 1
// parsed: Delivered
// False

When to use it

  • Store an order's state in a database column as a small integer with a readable name in code.
  • Replace a set of string constants like "pending" and "shipped" that are easy to misspell.
  • Switch over a fixed set of states and have the compiler list the members for you.
  • Validate a status read from a web request before acting on it.

More examples

Deciding what to do for each member

Enum.GetValues<TEnum>() returns every member as a real array of that enum type, sorted by underlying value, and was added in .NET 5. The switch covers each member by name, and the final arm handles a number with no member, which a cast can always produce. Reach for this when you need a label, a price, or a rule per member, and keep the final arm even when you have listed them all.

Example · csharp
OrderStatus[] all = Enum.GetValues<OrderStatus>();

foreach (OrderStatus value in all)
{
    Console.WriteLine($"{value} -> {Describe(value)}");
}

static string Describe(OrderStatus status) => status switch
{
    OrderStatus.Pending => "not sent yet",
    OrderStatus.Shipped => "on its way",
    OrderStatus.Delivered => "arrived",
    _ => "unknown"
};

enum OrderStatus
{
    Pending = 0,
    Shipped = 1,
    Delivered = 2
}

// Output:
// Pending -> not sent yet
// Shipped -> on its way
// Delivered -> arrived

Pinned numbers and a smaller underlying type

Any integer type can be the underlying type, and the gaps between 10, 20 and 30 leave room to add a member later without renumbering. Use explicit numbers for any enum that crosses a boundary: a database, a message queue, a JSON API. Use the default numbering only for an enum that never leaves the process.

Example · csharp
// The numbers are written out because this value is stored in a
// database column, so reordering the members must not change them.
Console.WriteLine((byte)Priority.High);
Console.WriteLine(Priority.High.ToString());

// The same number read back out of that column.
byte fromDatabase = 30;
Priority restored = (Priority)fromDatabase;
Console.WriteLine(restored);

// ": byte" picks the underlying type. Without it the type is int.
enum Priority : byte
{
    Low = 10,
    Normal = 20,
    High = 30
}

// Output:
// 30
// High
// High

Parsing untrusted text safely

Two surprises in one run. When parsing fails, the out value is not left alone: it is the default, which is whatever member has the value 0. And "7" parses successfully into a value with no name, which then prints as the number. Use this pair of checks on anything from a query string, a form field, or a config file.

Example · csharp
string[] inputs = ["Shipped", "shipped", "Teleported", "7"];

foreach (string input in inputs)
{
    // This overload is case-sensitive.
    bool ok = Enum.TryParse<OrderStatus>(input, out OrderStatus value);

    // TryParse accepts any number, so IsDefined is the second half
    // of the check.
    bool known = ok && Enum.IsDefined(value);

    Console.WriteLine($"{input}: parsed={ok} known={known} value={value}");
}

enum OrderStatus
{
    Pending = 0,
    Shipped = 1,
    Delivered = 2
}

// Output:
// Shipped: parsed=True known=True value=Shipped
// shipped: parsed=False known=False value=Pending
// Teleported: parsed=False known=False value=Pending
// 7: parsed=True known=False value=7

Discussion

  • Be the first to comment on this lesson.