Records, Structs & Enums

C# · Lesson 17 of 31 6 min read Updated

Many types in a program exist only to carry data: a book with a title and price, a point on a map, a student's marks. Writing a full class for each one, with properties, a constructor, equality and ToString, is a lot of typing. C# gives you records for exactly this job, structs for small values that should be copied, and enums for a fixed list of named options such as coach classes on a train.

In this lesson you will learn all three, and when to choose each one over a normal class.

Records: data in one line

A positional record declares its properties in brackets after the name. The compiler generates the constructor, init-only properties, a readable ToString, value-based equality and a Deconstruct method.

C#
Book b1 = new("Wings of Fire", "A. P. J. Abdul Kalam", 299m);
Book b2 = new("Wings of Fire", "A. P. J. Abdul Kalam", 299m);

Console.WriteLine(b1);
Console.WriteLine(b1 == b2);
Console.WriteLine(ReferenceEquals(b1, b2));

Book onSale = b1 with { Price = 249m };
Console.WriteLine(onSale);

var (title, author, price) = onSale;
Console.WriteLine($"{title} by {author}, now Rs {price}");

record Book(string Title, string Author, decimal Price);
Output
Book { Title = Wings of Fire, Author = A. P. J. Abdul Kalam, Price = 299 }
True
False
Book { Title = Wings of Fire, Author = A. P. J. Abdul Kalam, Price = 249 }
Wings of Fire by A. P. J. Abdul Kalam, now Rs 249

What the compiler gave you for free:

  • ToString that lists every property, which makes debugging easy.
  • Value equality: b1 == b2 is true because all property values match, even though they are two different objects (ReferenceEquals is false). For a normal class, == would compare references and print False.
  • Immutability: the properties are init-only, so b1.Price = 10m; does not compile.
  • with expressions: b1 with { Price = 249m } creates a copy with one property changed. The original b1 is untouched.
  • Deconstruction into separate variables.

Records can also have extra members. Note that the generated ToString includes every public property, computed ones too:

C#
var isha = new Marks("Isha", 91, 28);
Console.WriteLine(isha);
Console.WriteLine($"{isha.Student}: total {isha.Total}, passed: {isha.Passed}");

record Marks(string Student, int Maths, int Science)
{
    public int Total => Maths + Science;
    public bool Passed => Maths >= 33 && Science >= 33;
}
Output
Marks { Student = Isha, Maths = 91, Science = 28, Total = 119, Passed = False }
Isha: total 119, passed: False

Use records for data that is created, passed around and compared, but not changed in place: rows from a database, messages, API requests and responses, settings.

Structs: values that are copied

Classes and records are reference types. A struct is a value type: the variable holds the data itself, and assigning it to another variable copies all the data. int, double, decimal, bool and DateTime are all structs.

C#
Point a = new(2, 3);
Point b = a;      // copies the whole value
b.X = 10;

Console.WriteLine($"a = ({a.X}, {a.Y})");
Console.WriteLine($"b = ({b.X}, {b.Y})");

GeoPoint jaipur = new(26.9124, 75.7873);
GeoPoint copy = jaipur;
Console.WriteLine(jaipur);
Console.WriteLine(jaipur == copy);

struct Point
{
    public int X { get; set; }
    public int Y { get; set; }

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

readonly record struct GeoPoint(double Latitude, double Longitude);
Output
a = (2, 3)
b = (10, 3)
GeoPoint { Latitude = 26.9124, Longitude = 75.7873 }
True

Changing b did not change a, because b is a separate copy. Compare that with the bank account in Classes & Objects, where two variables shared one object.

A record struct (C# 10) combines the two ideas: a value type with the record's generated ToString, equality and with. Adding readonly makes every property read-only, which is the safest choice for small values.

Use a struct only for small values (a few numbers) that behave like a single value: coordinates, a colour, a money amount with currency. Large structs are slow to copy, and mutable structs cause confusing bugs when you change a copy by accident.

Enums: a fixed set of named values

An enum (enumeration) defines a type whose values come from a fixed list of names. Behind each name is an integer, starting at 0 by default.

C#
Coach booked = Coach.ThirdAc;

Console.WriteLine(booked);
Console.WriteLine((int)booked);
Console.WriteLine(Enum.Parse<Coach>("SecondAc"));
Console.WriteLine(Enum.TryParse("Business", out Coach other));

foreach (Coach c in Enum.GetValues<Coach>())
{
    Console.Write($"{c} ");
}
Console.WriteLine();

int fare = booked switch
{
    Coach.Sleeper => 450,
    Coach.ThirdAc => 1170,
    Coach.SecondAc => 1710,
    Coach.FirstAc => 2880,
    _ => throw new ArgumentOutOfRangeException(nameof(booked))
};
Console.WriteLine($"Fare: Rs {fare}");

enum Coach { Sleeper, ThirdAc, SecondAc, FirstAc }
Output
ThirdAc
1
SecondAc
False
Sleeper ThirdAc SecondAc FirstAc 
Fare: Rs 1170
  • Printing an enum gives its name; casting to int gives its number.
  • Enum.Parse<Coach>(text) converts text to an enum value and throws if the name is unknown; Enum.TryParse returns false instead.
  • Enum.GetValues<Coach>() lists every value, in order.
  • A switch expression over an enum still needs a _ arm, because an enum variable can technically hold any integer, such as (Coach)42. The fares are invented for the example.

Flags enums: combining options

Sometimes several options can be on at once, like the amenities of a hotel room. Give each value its own bit (1, 2, 4, 8...) and mark the enum with the [Flags] attribute. Then combine values with |.

C#
Amenities room = Amenities.Wifi | Amenities.Breakfast;
Console.WriteLine(room);
Console.WriteLine(room.HasFlag(Amenities.Ac));

room |= Amenities.Ac;
Console.WriteLine(room);

[Flags]
enum Amenities
{
    None = 0,
    Wifi = 1,
    Ac = 2,
    Breakfast = 4,
    Parking = 8
}
Output
Wifi, Breakfast
False
Wifi, Ac, Breakfast

HasFlag checks whether an option is switched on, and |= switches one on.

Choosing the right type

UseWhen
classObjects with identity and changing state: a bank account, a game player
recordData that is compared by value and copied with with: a book, an order line
struct / readonly record structSmall values of a few fields: a point, a colour
enumA fixed set of named options: coach class, order status

Common mistakes

  • Trying to change a record property. Positional record properties are init-only. Use with to make a changed copy.
  • Expecting a struct change to affect the original. Structs are copied on assignment and when passed to methods.
  • Making big or mutable structs. Keep structs small, and prefer readonly.
  • Enum.Parse with the wrong case. Enum.Parse<Coach>("thirdac") throws. Pass ignoreCase: true or use TryParse.
  • Storing enum numbers in a database. If someone reorders the enum, the saved numbers change meaning. Give values explicit numbers or store the names.

Key points

  • record Name(Type A, Type B); creates an immutable data type with ToString, value equality, with and deconstruction.
  • Structs are value types: assignment copies the data. readonly record struct is ideal for small values.
  • Classes and records are reference types; equality for records compares values.
  • An enum is a fixed set of named integer values; use Enum.Parse, TryParse and GetValues.
  • [Flags] enums combine options with | and test them with HasFlag.

Exercise

Create a record Parcel(string Id, string City, decimal WeightKg, DeliveryStatus Status) and an enum DeliveryStatus { Booked, InTransit, Delivered, Returned }. Create a parcel P101 for Pune weighing 2.5 kg with status Booked. Use with to create the "in transit" version and then the "delivered" version. Print all three, and check whether the first and last parcels are equal.

Show solution
C#
Parcel booked = new("P101", "Pune", 2.5m, DeliveryStatus.Booked);
Parcel moving = booked with { Status = DeliveryStatus.InTransit };
Parcel done = moving with { Status = DeliveryStatus.Delivered };

Console.WriteLine(booked);
Console.WriteLine(moving);
Console.WriteLine(done);
Console.WriteLine(booked == done);

record Parcel(string Id, string City, decimal WeightKg, DeliveryStatus Status);

enum DeliveryStatus { Booked, InTransit, Delivered, Returned }
Output
Parcel { Id = P101, City = Pune, WeightKg = 2.5, Status = Booked }
Parcel { Id = P101, City = Pune, WeightKg = 2.5, Status = InTransit }
Parcel { Id = P101, City = Pune, WeightKg = 2.5, Status = Delivered }
False

Each with produces a new record and leaves the earlier one unchanged, so you keep a history of states. The first and last parcels differ in Status, so value equality returns False.

Quick Quiz

1. Two record objects have identical property values. What does == return?

2. What does var p2 = p1 with { City = "Kochi" }; do for a record p1?

3. Which statement about structs is true?

4. For enum Size { Small, Medium, Large }, what is (int)Size.Large?

Learn C# with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call