Importantly, the record modifier is not limited to creating value types. Starting in C# 9, support was added for record class as demonstrated by Listing 9.4.
In this record class we have used the Angle type as positional parameters into the Coordinate to represent longitude and latitude.
To distinguish record structs and classes from their non-record versions we will refer to them as standard structs and standard classes within this chapter. One of the most critical questions that the record class feature introduces is when should you use record class rather than a standard class, and vice-versa. The key, and perhaps the only reason to define a record class is if the type needs to have value equality behavior. In fact, whenever a custom type needs to implement value equality (always the case for a value type) the record construct should be used if possible. An example of when it might not be possible is working with a C# version prior to C# 9 (for a reference type) or C# 10.0 (for a value type).
Another reason why the record construct cannot be used on a class is when the base class is not a record. A record class can only inherit from another record class while a standard class can only inherit from another standard class. – the inheritance chain cannot mix and match between standard classes and records.
Many of the features associated with record struct are also used in record classes, including: succinct declaration, data storage with properties, constructor initialization, deconstruction, ToString() diagnostic capabilities, and perhaps most importantly because of the complexities, value equivalence, not just reference equivalence. The equivalent record class generated C# code is shown in Listing 9.5.
There are several expected differences in the records struct verses record class generated code.
Firstly, several members are decorated with virtual modifier (PrintMembers(StringBuilder builder), Equals(Coordinate? Other), and EqualityContract()). Virtual was never used in the record struct since all structs are sealed, making virtual non-sensical in the record struct case.
Secondly, a record class needs to account for possibility of a null. Thus, Equals(), the == operator, and both of the Equals() methods need to check for a null parameter value.
In the next two sections we will explore records in detail, discuss each feature, it’s implementation where noteworthy, and the unique differences between the record struct and record class implementations.