
In the previous article, we analyzed the Positional Records that are the real innovation of this new functionality of C# 9. We discovered that behind the scenes a record is nothing but a class with specific default behaviors, including the immutability and equality of values.
Would it therefore be possible to write a record with a syntax similar to that of a class? Yes, of course! Let’s see it:
public record CustomRecordPerson
{
public string Name { get; set; }
public string Surname { get; set; }
public CustomRecordPerson(string name, string surname)
{
Name = name;
Surname = surname;
}
public CustomRecordPerson()
{
}
}
As we can see, the only difference with the definition of a class, consists of using the record keyword instead of the keyword class.
We can therefore create an instance of our record through the constructor:
var customRecordPerson = new CustomRecordPerson(name: "Francesco", surname:"Vas");
Or through an object initializer, which, I remind you, we can use it only if we also have a constructor without parameters:
var customRecordPerson2 = new CustomRecordPerson
{
Name = "Francesco",
Surname = "Vas"
};
Now let’s try to modify one of our record’s the properties:
customRecordPerson.Name = "Adolfo";
We know that the properties of a record are immutable and for someone could therefore be a surprise that the compiler does not signal an error. Despite this supposition, we can change the value of a property without problems!
We must not mistakenly think that a record is an always immutable data structure. Properties passed as an argument in the case of positional records are always immutable.
As we have seen, these are set by default as init-only properties. We can do the same in our case to get the immutability:
public record CustomRecordPerson
{
public string Name { get; init; }
public string Surname { get; init; }
//...
}
Instead of the classic setter type accessor method, we need to use the init-only setter type accessor method for properties that we want to make immutable.
We have already talked about the keyword init, and we know that it allows us to specify that the value of that property can only be set when the object is initialized but cannot be changed later.
We will deepen even more by saying that this new feature allows us to obtain a read-only backing-field for the property, which the compiler generates for us behind the scenes:
[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string <Name>k__BackingField;
So basically, using the init keyword for a property of a class or a record is the same as writing:
public class Person
{
private readonly string _name;
public Person(string name)
{
Name = name;
}
public string Name
{
get => _name;
init => _name = value;
}
}
If we try to replace the init keyword with the keyword set, the compiler would signal us an error:

It happens because the init-only type accessor methods are enabled to modify read-only fields.
Here are all the secrets behind the immutability introduced in C # 9!
Regardless of the use of the immutability and therefore of the init keyword, obviously also a record written with this alternative syntax provides us with some of the features we have already seen for positional records. Let us then consider the following definition of a record, in which we specified only two properties
public record CustomRecordPerson
{
public string Name { get; init; }
public string Surname { get; init; }
}
And let’s analyze the generated IL code:
public class CustomRecordPerson : IEquatable<CustomRecordPerson>
{
protected virtual Type EqualityContract
{
[System.Runtime.CompilerServices.NullableContext(1)]
[CompilerGenerated]
get
{
return typeof(CustomRecordPerson);
}
}
public string Name { get; init; }
public string Surname { get; init; }
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("CustomRecordPerson");
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append("Name");
builder.Append(" = ");
builder.Append((object?)Name);
builder.Append(", ");
builder.Append("Surname");
builder.Append(" = ");
builder.Append((object?)Surname);
return true;
}
[System.Runtime.CompilerServices.NullableContext(2)]
public static bool operator !=(CustomRecordPerson? r1, CustomRecordPerson? r2)
{
return !(r1 == r2);
}
[System.Runtime.CompilerServices.NullableContext(2)]
public static bool operator ==(CustomRecordPerson? r1, CustomRecordPerson? r2)
{
return (object)r1 == r2 || (r1?.Equals(r2) ?? false);
}
public override int GetHashCode()
{
return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 +
EqualityComparer<string>.Default.GetHashCode(Name)) * -1521134295 +
EqualityComparer<string>.Default.GetHashCode(Surname);
}
public override bool Equals(object? obj)
{
return Equals(obj as CustomRecordPerson);
}
public virtual bool Equals(CustomRecordPerson? other)
{
return (object)other != null && EqualityContract == other!.EqualityContract &&
EqualityComparer<string>.Default.Equals(Name, other!.Name) &&
EqualityComparer<string>.Default.Equals(Surname, other!.Surname);
}
public virtual CustomRecordPerson <Clone>$()
{
return new CustomRecordPerson(this);
}
protected CustomRecordPerson(CustomRecordPerson original)
{
Name = original.Name;
Surname = original.Surname;
}
public CustomRecordPerson()
{
}
}
We will not dwell on analyzing all the features, which we have already discussed in the previous article on positional records. We will only analyze the differences between the features offered automatically by the two different ways of writing a record.
As we see in this case, we do not automatically obtain the parameterized constructor, and therefore just as for a normal class, we have been provided by the default constructor. We still have all the methods and overrides supporting structural equality and the ToString() method override, which provides us text representation of the type and values of the properties of the record.
But the deconstructor lacks.
For both syntaxes, there are a protected constructor and a virtual method Clone(), which we have not treated previously. Still, the behavior of non-destructive mutation implemented by record is based on them, that is the operation of the with keyword.
Regardless of the syntax used, records support inheritance. In fact, a record can inherit from another record and can also be abstract type.
But a record cannot inherit from a class, and a class cannot inherit from a record.
We can also use generic constraints, and even if there is no generic constraint for the records, the records satisfy the class constraint. This means that we could use, for example, generic constraints in these ways:
public record GenericRecord<T> where T : class
{
}
public class GenericClass<T> where T : CustomRecordPerson
{
}
We finish here the deepening on the records.
We have already talked about the possible fields of use of the records. We have learned that they are nothing but classes with specific default behaviors and that there are two types of syntax to write them that provide us more or less the same functionalities.
Honestly, without particular needs in terms of customization, I believe that the syntax of positional records is much more useful because it allows us an obvious savings of quantity of written code and, therefore, development time.
When is it better to use a record instead of a class?
Mostly, if we want to define a model that depends on the equality of values and if we want to determine a reference type for which objects are not editable.
I hope you found the topic interesting, see you in the next article!
Stay Tuned!