First and foremost: the keyword, record
, is of the same category as the keywords struct and class. It has been introduced with C# 9.0. Let's study it threadbare!
class vs struct vs record
This is a summary chart of the status of class, struct and record as of now -
- ref/value type classification
-
class: reference type
struct: value type
record: reference type
- Member Properties:
-
class: no restrictions: the properties and data members can be mutable (i.e, alterable) as well as immutable.
struct: no restrictions: the properties and data members can be mutable (i.e, alterable) as well as immutable.
record: they are designed to be immutable, and they indeed are, if positional syntax [explained later] is used for defining a record, or if the setters are explicitly marked non-public. But if the properties have public setters, then they are mutable i.e., they can be changed anytime.
class: two instances are equal if their references are equal.
struct: equality is determined by the equality of values of the data members - but run-time overheads can be there, such as due to reflection. However, a software developer can provide his own over-rides to make the comparison efficient.
record: equality is determined by the equality of values of the data members - the methods and operators for comparison are generated by the compiler, and, therefore, efficient - basically it saves the software developer from writing his own over-rides.
class: there is no compiler provided copy constructor; a software developer has to write his own.
struct: struct values are by default shallow-copied on assignment, passing an arg, or returning from a function call. A software developer can write his own copy constructor, if he needs one, say, for example, for deep copying.
record: complier provides the ability for explicit copy through with
expressions. For example: var dest = src with {}
creates "dest" as a [shallow, i.e., member-wise] copy of a pre-existing record called "src". Copy constructors are synthesized by the compiler - but they are protected
.
class: passed as references, and no copies are created.
struct: passed as a value, shallow copies are created, boxing/ unboxing can occur, unless ref/ out/ in/"ref returns" are specified
record: passed as references, and no copies are created.
class: yes
struct: no
record: yes! And the copy through with
expressions takes care of copying the whole chain of inherited properties.
class: available
struct: available
record: available, but PrintMembers
is also available, and provides a better formatted, comma-separated, display
class: general
struct: to hold small, related data. Why "small"? because there is an overhead of copying/ boxing/ unboxing during function calls. The best example is the Point structure that holds int x and y co-ordinates
record: concurrent programs with shared data; the record
instances are immutable, and safely passed around as references without the overhead of copying, boxing/ unboxing. Can be used to hold large data that can be passed around efficiently.
The Methods of a record
that the Compiler Synthesizes
Following is a quick summary of the most important ones:
- The operators
==
and!=
- Comparison operators
==
and!=
compare the respective values of the properties with regard to their data-types. - GetHashCode
- The compiler produces an over-ride of GetHashCode by using the GetHashCode from all the properties of the record, including those from the parent classes! This ensures that a comparison is with regard to not just the values, but also to the types of the records being compared. This method is not directly used by application developers, but the compiler probably uses it for the equality operators.
- Copy Constructor
- The compiler synthesizes a protected copy constructor. It also creates an internal clone method. The clone method, too, is not directly used by application developers. But the compiler probably uses it for providing the ability to copy records through
with
expressions.
How to Create a Copy?
Suppose we have a record defined such:
public record PinPoint { // properties with setter // marked as init - means // init-once public int x { get; init;} // property for y public int y { get; init;} // ctor public PinPoint(int _x, int _y) => (x, y) = (_x, _y); }
Below are two examples - the first one creates an exact same copy, and the second one modifies a property during copy -
// a new object PinPoint pp1 = new PinPoint(2, 4); // exact same copy // pp2.x will be 2 and pp2.y will be 4 PinPoint pp2 = pp1 with { }; // copy with modification // pp2.x will be 8 and pp2.y will be 4 PinPoint pp3 = pp1 with { x = 8 };
What is Non-Destructive Mutation?
This term has started getting attention recently after Microsoft C# 9.0 introduced record. There's no big deal about it as you can see below:In the example discussed above, we have created pp2 and pp3 as mutations [i.e., copies] of pp1. The data of pp1 remains same at x = 2, y = 4 after the copy. There is no impact on pp1 during the copy process - its data remains same at x = 2, and y = 4. It has NOT GIVEN its data - it has created mutations of itself, without affecting itself. This is termed as Non-Destructive Mutation.
What are Positional Records?
Positional records is a syntax of defining a record. The setters on the records are marked as init
, to make them init-once.
// a record with just the properties x and y // defined with the positional syntax public record PinPoint (int x, int y);
What is the Deconstruct Method?
IMPORTANT: Deconstruct method is generated by the compiler - BUT it is done only if the record is defined with the positional records syntax. Otherwise you can create your own Deconstruct method with "out
" parameters. Visual Studio 16.9+ has a quick-fix for this - you can check that out. "Deconstruct" method is used to quickly extract the data out of a record as shown here -
// suppose we have a PinPoint record PinPoint p1 = new PinPoint(2, 4); // extract both the props into px and py // the deconstruct method works behind the scenes (int px, int py) = p1; // px is 2, and py is 4
Inheritance with Records
The following derives a record called BallPoint, with three properties, from a PinPoint. We have used the positional syntax.
public record BallPoint (int x, int y, int z) : PinPoint (x, y); // create a ballpoint BallPoint bp = new BallPoint(2, 3, 4);
What is PrintMembers? What is its use?
PrintMembers is a method similar to the classic ToString method. It is available on a record
and displays its alternate string representation. It accepts a StringBuilder as an argument.
PrintMembers can be found useful to dump the contents of a record including the members from the inheritance chain.
// suppose we have a PinPoint record BallPoint p1 = new BallPoint(2, 4, 8); StringBuilder sb = new StringBuilder(); p1.PrintMembers(sb); Console.WriteLine(sb.ToString()); // dumps the data like thus // BallPoint {x = 2, y = 4, z = 8}
This Blog Post/Article "(C# 9.0) Record types vs class vs struct type, positional records and non-destructive mutation" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.