Day 13: Enums and Structs - When Classes Are Too Extra
arrow_back All Posts
April 06, 2026 7 min read .NET/C#

Day 13: Enums and Structs - When Classes Are Too Extra

Yesterday you signed contracts with interfaces — your classes promised to do things, and the compiler held them to it. Today we're going smaller. Not everything in life needs to be a full-blown class with inheritance and polymorphism and existential dread. Sometimes you just need a simple label. Sometimes you need a lightweight data container. Enter enums and structs — the minimalists of the C# type system.

1. Enums — Giving Names to Numbers

An enum (short for enumeration) is a way to define a set of named constants. Instead of passing around magic numbers like 1, 2, 3 and hoping everyone remembers that 3 means "cancelled," you give them proper names.

Think of an enum as a restaurant menu with numbered items. You could walk up and say, "I'll have number 7." But wouldn't it be nicer to say, "I'll have the Margherita Pizza"? Enums let your code say what it means instead of mumbling numbers.

enum OrderStatus
{
    Pending,    // 0
    Processing, // 1
    Shipped,    // 2
    Delivered,  // 3
    Cancelled   // 4
}

By default, the first value is 0, and each subsequent value increments by one. But you don't usually care about the numbers — that's the whole point.

2. Using Enums in Code

enum Season
{
    Spring,
    Summer,
    Autumn,
    Winter
}

Season currentSeason = Season.Summer;
Console.WriteLine(currentSeason);        // Output: Summer
Console.WriteLine((int)currentSeason);   // Output: 1

if (currentSeason == Season.Summer)
{
    Console.WriteLine("Time for ice cream! 🍦");
}

// Switch works beautifully with enums
string clothing = currentSeason switch
{
    Season.Spring => "Light jacket",
    Season.Summer => "T-shirt and shorts",
    Season.Autumn => "Sweater",
    Season.Winter => "Everything you own",
    _ => "Just stay home"
};
Console.WriteLine(clothing); // T-shirt and shorts

Notice how the switch expression covers every case? The compiler can even warn you if you forget a case in a switch over an enum. It's like having a polite secretary who taps you on the shoulder and says, "Excuse me, you forgot about Winter."

3. Enum Tricks — Custom Values and Underlying Types

You're not stuck with 0, 1, 2, 3. You can assign custom values:

enum HttpStatus
{
    OK = 200,
    NotFound = 404,
    InternalServerError = 500,
    ImATeapot = 418  // Yes, this is a real HTTP status code
}

Console.WriteLine((int)HttpStatus.ImATeapot); // 418

And enums aren't limited to int. You can pick any integer type:

enum Permission : byte
{
    None = 0,
    Read = 1,
    Write = 2,
    Execute = 4
}

Why byte? If you're storing millions of these values, using 1 byte instead of 4 saves real memory. Most of the time, though, int (the default) is perfectly fine.

4. Flags — When You Need Multiple Options at Once

Sometimes one choice isn't enough. A file might be both readable and writable. The [Flags] attribute lets you combine enum values using bitwise operations:

[Flags]
enum FilePermission
{
    None    = 0,
    Read    = 1,
    Write   = 2,
    Execute = 4,
    All     = Read | Write | Execute  // 7
}

FilePermission myPermissions = FilePermission.Read | FilePermission.Write;
Console.WriteLine(myPermissions);  // Output: Read, Write

// Check if a specific flag is set
bool canWrite = myPermissions.HasFlag(FilePermission.Write);
Console.WriteLine($"Can write: {canWrite}"); // Can write: True

bool canExecute = myPermissions.HasFlag(FilePermission.Execute);
Console.WriteLine($"Can execute: {canExecute}"); // Can execute: False

The values are powers of 2 (1, 2, 4, 8, ...) so they can be combined without overlap. If you used 1, 2, 3, combining Read (1) and Write (2) would give you 3 — which is the same as the value you assigned to some other option. Chaos.

Pro tip: Always use powers of 2 for [Flags] enums. 0, 1, 2, 4, 8, 16, 32... — it's the binary system doing its thing.

5. Structs — Lightweight Value Types

Now let's talk about structs. If a class is a house — foundation, plumbing, electrical, mortgage — a struct is a tent. Quick to set up, no overhead, and you carry it with you.

A struct is a value type. When you assign it to a new variable or pass it to a method, C# makes a copy. Compare this to a class (a reference type), where variables point to the same object in memory.

struct Point
{
    public double X;
    public double Y;

    public Point(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double DistanceTo(Point other)
    {
        double dx = X - other.X;
        double dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }

    public override string ToString() => $"({X}, {Y})";
}

Point a = new Point(3, 4);
Point b = a;  // COPY — b is a completely independent clone
b.X = 99;

Console.WriteLine(a); // (3, 4) — unchanged!
Console.WriteLine(b); // (99, 4) — only the copy changed

With a class, changing b.X would also change a.X because they'd point to the same object. With a struct, each variable has its own data. No surprises, no spooky action at a distance.

6. Value Type vs Reference Type — The Big Picture

This concept trips up a lot of beginners, so let's make it clear:

Value types (stored on the stack):

  • int, double, bool, char, decimal — the built-in primitives
  • struct — your custom value types
  • enum — also a value type under the hood!

Reference types (stored on the heap):

  • class — your custom reference types
  • string — technically a reference type (but behaves like a value type because it's immutable)
  • Arrays, delegates, interfaces
// Value type: copy semantics
int x = 10;
int y = x;
y = 99;
Console.WriteLine(x); // 10 — x is untouched

// Reference type: shared reference
int[] arr1 = [1, 2, 3];
int[] arr2 = arr1;
arr2[0] = 99;
Console.WriteLine(arr1[0]); // 99 — arr1 was also changed!

When to use a struct instead of a class:

  • The data is small (16 bytes or less is the rule of thumb).
  • It logically represents a single value (a point, a color, a date).
  • It's immutable or rarely mutated.
  • You need high performance with lots of instances (structs avoid heap allocation and garbage collection overhead).

If in doubt, use a class. Structs are an optimization, not the default.

7. readonly struct — Immutability by Design

C# lets you mark a struct as readonly to guarantee it never changes after creation:

readonly struct Temperature
{
    public double Celsius { get; }
    public double Fahrenheit => Celsius * 9.0 / 5.0 + 32;

    public Temperature(double celsius)
    {
        Celsius = celsius;
    }

    public override string ToString() => $"{Celsius}°C ({Fahrenheit:F1}°F)";
}

Temperature boiling = new Temperature(100);
Console.WriteLine(boiling); // 100°C (212.0°F)

// boiling.Celsius = 50; // ❌ Compiler error — it's readonly!

Why bother? Two reasons:

  1. Safety — you can't accidentally mutate the struct.
  2. Performance — the compiler can optimize readonly struct more aggressively because it knows the data won't change.

The built-in DateTime, TimeSpan, and Guid types in .NET are all structs. You've been using value types this whole time without realizing it!

8. Your Homework: Build a Card Game Foundation

Create a console app with:

  1. A [Flags] enum isn't needed here — just a regular Suit enum: Hearts, Diamonds, Clubs, Spades.
  2. An enum Rank with values Two through Ace (use custom values: Two = 2, Three = 3, ... Ten = 10, Jack = 11, Queen = 12, King = 13, Ace = 14).
  3. A readonly struct Card with Suit and Rank properties, plus a ToString() override.
  4. Create a few cards and print them out.
enum Suit { Hearts, Diamonds, Clubs, Spades }

enum Rank
{
    Two = 2, Three, Four, Five, Six, Seven,
    Eight, Nine, Ten, Jack, Queen, King, Ace
}

readonly struct Card
{
    public Suit Suit { get; }
    public Rank Rank { get; }

    public Card(Suit suit, Rank rank)
    {
        Suit = suit;
        Rank = rank;
    }

    public override string ToString() => $"{Rank} of {Suit}";
}

// Deal some cards!
Card aceOfSpades = new Card(Suit.Spades, Rank.Ace);
Card twoOfHearts = new Card(Suit.Hearts, Rank.Two);
Card queenOfDiamonds = new Card(Suit.Diamonds, Rank.Queen);

Console.WriteLine(aceOfSpades);       // Ace of Spades
Console.WriteLine(twoOfHearts);       // Two of Hearts
Console.WriteLine(queenOfDiamonds);   // Queen of Diamonds

// Compare cards by rank
if (aceOfSpades.Rank > queenOfDiamonds.Rank)
{
    Console.WriteLine($"{aceOfSpades} beats {queenOfDiamonds}!");
}

Bonus challenge: Create a List<Card> representing a full 52-card deck using nested foreach loops over Enum.GetValues<Suit>() and Enum.GetValues<Rank>(). Print all 52 cards.

Summary of Day 13

  • Enums give meaningful names to constant values — no more magic numbers.
  • Enums use int by default but can use any integer type (byte, short, long).
  • [Flags] enums use powers of 2 to let you combine multiple values with bitwise OR.
  • Structs are value types — assigning or passing them creates a copy, not a shared reference.
  • Use structs for small, simple, value-like data (points, colors, coordinates).
  • readonly struct guarantees immutability and enables compiler optimizations.
  • Both enums and structs are value types that live on the stack — fast allocation, no garbage collection overhead.

Tomorrow: we'll talk about Generics — writing code that works with any type without sacrificing type safety. Imagine a box that can hold an int, a string, or a Pizza — and always knows what's inside. 📦

See you on Day 14!

Share
FM

Farhad Mammadov

.NET Engineer & Cloud Architect · Bayern, Germany. Writing about scalable backend systems, AWS, and SRE.