Records, Structs & Enums
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.
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);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 249What the compiler gave you for free:
ToStringthat lists every property, which makes debugging easy.- Value equality:
b1 == b2istruebecause all property values match, even though they are two different objects (ReferenceEqualsisfalse). For a normal class,==would compare references and printFalse. - Immutability: the properties are
init-only, sob1.Price = 10m;does not compile. withexpressions:b1 with { Price = 249m }creates a copy with one property changed. The originalb1is untouched.- Deconstruction into separate variables.
Records can also have extra members. Note that the generated ToString includes every public property, computed ones too:
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;
}Marks { Student = Isha, Maths = 91, Science = 28, Total = 119, Passed = False }
Isha: total 119, passed: FalseUse 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.
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);a = (2, 3)
b = (10, 3)
GeoPoint { Latitude = 26.9124, Longitude = 75.7873 }
TrueChanging 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.
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 }ThirdAc 1 SecondAc False Sleeper ThirdAc SecondAc FirstAc Fare: Rs 1170
- Printing an enum gives its name; casting to
intgives its number. Enum.Parse<Coach>(text)converts text to an enum value and throws if the name is unknown;Enum.TryParsereturnsfalseinstead.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 |.
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
}Wifi, Breakfast False Wifi, Ac, Breakfast
HasFlag checks whether an option is switched on, and |= switches one on.
Choosing the right type
| Use | When |
|---|---|
class | Objects with identity and changing state: a bank account, a game player |
record | Data that is compared by value and copied with with: a book, an order line |
struct / readonly record struct | Small values of a few fields: a point, a colour |
enum | A fixed set of named options: coach class, order status |
Common mistakes
- Trying to change a record property. Positional record properties are
init-only. Usewithto 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.Parsewith the wrong case.Enum.Parse<Coach>("thirdac")throws. PassignoreCase: trueor useTryParse.- 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 withToString, value equality,withand deconstruction.- Structs are value types: assignment copies the data.
readonly record structis ideal for small values. - Classes and records are reference types; equality for records compares values.
- An
enumis a fixed set of named integer values; useEnum.Parse,TryParseandGetValues. [Flags]enums combine options with|and test them withHasFlag.
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
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 }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 }
FalseEach 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?
Records generate value-based equality, so equal property values mean equal records.
2. What does var p2 = p1 with { City = "Kochi" }; do for a record p1?
with creates a new record; the original stays unchanged.
3. Which statement about structs is true?
Structs are value types, so assignment copies the value.
4. For enum Size { Small, Medium, Large }, what is (int)Size.Large?
Enum values start at 0 by default, so Large is 2.
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
