Understanding the Differences Between C# Records and Classes
Written on
Chapter 1: Introduction to C# Records
With the launch of C# 10 in November 2021, developers gained access to a new feature known as records. This document will explore the main distinctions between records and classes. For the sake of this analysis, I'll focus on the standard methods of defining classes and records. Essentially, a record is syntactic sugar built on top of a class or struct, resulting in differing default behaviors.
For our comparison, we will use the following record definition:
public record PointRecord(int X, int Y);
And the corresponding class definition:
public class PointClass
{
public int X { get; set; }
public int Y { get; set; }
}
Section 1.1: Key Differences
1. Mutability
Records are designed to be immutable, meaning their properties can only be set during creation. Conversely, a class can be designed to behave similarly by using { get; init; } instead of { get; set; }. With a class, properties can be altered post-creation, while attempting to change properties in a record will result in a compile-time error.
2. Equality
In classes, equality is determined by reference, while for records, it is based on value. For instance, two instances of PointClass, even if they have identical properties, will not be considered equal due to their differing references. In contrast, two PointRecords with the same properties will be deemed equal. Although it is possible to override equality behavior in classes by implementing the equals method, records provide this functionality by default.
3. Deconstruction
Deconstruction allows for breaking a variable's value into individual components and assigning them to new variables. This can be particularly beneficial for variables that hold multiple values, such as tuples. For instance, when iterating over a list of PointClass instances:
var points = new List<PointClass>();
foreach (var point in points)
{
var z = point.X * point.Y;
}
The properties X and Y can only be accessed through the point instance. Records simplify this process through deconstruction:
var points = new List<PointRecord>();
foreach (var (x, y) in points)
{
var z = x * y;
}
Classes can also support deconstruction by implementing a method named Deconstruct:
public class PointClass
{
public int X { get; set; }
public int Y { get; set; }
public void Deconstruct(out int x, out int y)
{
x = this.X;
y = this.Y;
}
}
4. ToString()
The ToString() method in a class will return the class name unless it is overridden. However, in records, this method is overridden to display both the class name and its properties, which is especially useful for debugging as it allows for easier visualization of values.
5. GetHashCode()
For reference types, hash codes are generated by invoking the Object.GetHashCode method from the base class, which computes a hash code based on the object's reference unless overridden. In contrast, the hash code for records is derived from the values of their properties. Thus, two class instances with identical properties will yield different hash codes, while two records with the same values will generate the same hash code.
Chapter 2: The Underlying Implementation of Records
The accompanying video titled "C Programming Tutorial for Beginners" provides additional insights into the practical aspects of using records and classes in C#. In this tutorial, viewers can gain a deeper understanding of programming fundamentals, including practical coding examples.
The lowered C# representation of a record reveals the implementation nuances, illustrating why records exhibit distinct behaviors compared to classes. The key overridden methods are:
public override string ToString()
public override int GetHashCode()
public override bool Equals(object obj)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
[NullableContext(1)]
[Nullable(0)]
public class PointRecord : IEquatable<PointRecord>
{
[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private int k__BackingField;
[CompilerGenerated]
protected virtual Type EqualityContract => typeof(PointRecord);
public int X
{
[CompilerGenerated] get => this.k__BackingField;
[CompilerGenerated] set => this.k__BackingField = value;
}
public int Y
{
[CompilerGenerated] get => this.k__BackingField;
[CompilerGenerated] set => this.k__BackingField = value;
}
[CompilerGenerated]
public override string ToString()
{
StringBuilder builder = new StringBuilder();
builder.Append("PointRecord { ");
if (this.PrintMembers(builder))
builder.Append(' ');builder.Append('}');
return builder.ToString();
}
[CompilerGenerated]
protected virtual bool PrintMembers(StringBuilder builder)
{
RuntimeHelpers.EnsureSufficientExecutionStack();
builder.Append("X = ");
builder.Append(this.X.ToString());
builder.Append(", Y = ");
builder.Append(this.Y.ToString());
return true;
}
// Further implementation details...
}