Records
An enum holds one value from a list. Often you need something else: to keep several related pieces of data together and compare them as a whole.
Think of an office pass: it carries a name and a room number. The guard at the door compares the data, not the card itself. If you lose your pass and get a new one printed with the same data, for the guard it is the same pass.
An object of a class is compared differently. In the theme on value and reference types, == on two class objects with equal fields gave false. It checks whether it is one card, not whether the data on the cards is the same.
For data that is compared by its contents, C# has records—types declared with the word record:
var a = new Point(1, 2);
var b = new Point(1, 2);
Console.WriteLine(a == b); // True
Console.WriteLine(a); // Point { X = 1, Y = 2 }
record Point(int X, int Y);Let's take the program apart line by line:
record Point(int X, int Y);declares the recordPointwith two properties:XandY. The whole declaration fits on one line.new Point(1, 2)creates a record the same waynewcreates a class object:Xgets1,Ygets2.a == bgivestrue:aandbare two different records, but their data is the same.Console.WriteLine(a)prints the type's name and all the data:Point { X = 1, Y = 2 }.
What one record line gives you
The record Point is called positional: its data is listed in parentheses in order, and you pass it to new in the same order. From this line, the compiler writes for you what a class would need written by hand:
- the properties
XandY: you can read them througha.X, but not change them after creation; - a constructor with the parameters
XandY, so bothnew Point(1, 2)and a call with named arguments,new Point(X: 1, Y: 2), work; - comparison by data:
==,!=andEqualscompare the properties one by one; - a
ToStringmethod that returns the stringPoint { X = 1, Y = 2 }for printing.
A record and a class
Compare the record with a class that holds the same data:
var c1 = new Spot(1, 2);
var c2 = new Spot(1, 2);
Console.WriteLine(c1 == c2); // False
Console.WriteLine(c1); // Spot
class Spot
{
public int X;
public int Y;
public Spot(int x, int y) { X = x; Y = y; }
}c1 and c2 hold the same data, but == compares references, and they lead to two separate objects. Printing shows only the class name: a class's ToString doesn't know by default what is in its fields.
Interviewers often ask how a record differs from a class. The short answer:
- a record is compared by its data, a class object by its reference;
- a positional record gets its properties, constructor and
ToStringfor free, and thatToStringreturns a string with all its data; - the properties of a positional record cannot be changed after creation.
This is where the pass analogy breaks down. A real pass cannot sit in two pockets at once. But a record is a reference type, like a class: after var b = a;, two names lead to the same record. For Point that is safe: the properties of a positional record cannot be changed, so nobody can replace X or Y of a through b.
Common mistake
Someone tried to change a property of a record:
var a = new Point(1, 2);
a.X = 5; // ✗
record Point(int X, int Y);error CS8852: Init-only property or indexer 'Point.X' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.The program doesn't compile. The properties of a positional record are init properties: they get their values only at creation. To get a point with X = 5, create a new record: a = new Point(5, a.Y);.
Key points
record Point(int X, int Y);declares a record with the propertiesXandY, a constructor and aToStringthat givesPoint { X = 1, Y = 2 }.- Records are compared by data: two records with equal properties are equal, and
==givestrue. - On class objects,
==compares references, so two objects with equal fields are not equal. - The properties of a positional record cannot be changed after creation:
a.X = 5;doesn't compile.