Customizing the behavior of record copy constructors
When you use a record
. you can create a new instance by using the new
keyword. Or you can copy an instance with some modifications using with
expression (non-destructive mutation). The with
expression copy all fields from the original instance and then apply the modifications.
var john = new Sample("John", "Doe");
var Jane = john with { FirstName = "Jane" };
record Person(string FirstName, string LastName);
If the record contains a mutable object, such as a List<T>
, the instance will be shared between the original and the copy. This is because the with
expression doesn't clone the mutable object. Here's an example:
var john = new Person("John", "Doe") { PhoneNumbers = new() { "1234567890" } };
var jane = john with { FirstName = "Jane" };
jane.PhoneNumbers.Add("123");
// Both list are modified
Console.WriteLine(john.PhoneNumbers.Count); // 2
Console.WriteLine(jane.PhoneNumbers.Count); // 2
public record Person(string FirstName, string LastName)
{
public List<string>? PhoneNumbers { get; init; }
}
When you use the with
expression, the C# compiler calls the generated <Clone>$
method. This method calls the copy constructor. If you don't implement a copy constructor, the compiler will generate one for you that copies all fields. Here's the generated code for the Person
record:
internal class Person
{
// ...
[CompilerGenerated]
public virtual Person <Clone>$()
{
return new Person(this);
}
[CompilerGenerated]
protected Person(Person original)
{
<FirstName>k__BackingField = original.<FirstName>k__BackingField;
<LastName>k__BackingField = original.<LastName>k__BackingField;
<PhoneNumbers>k__BackingField = original.<PhoneNumbers>k__BackingField;
}
}
If you want to clone the PhoneNumbers
property when using the with
expression, you can implement a copy constructor manually:
public record Person(string FirstName, string LastName)
{
public List<string>? PhoneNumbers { get; init; }
protected Person(Person other)
{
FirstName = other.FirstName;
LastName = other.LastName;
if(other.PhoneNumbers != null)
{
PhoneNumbers = new List<string>(other.PhoneNumbers);
}
}
}
//
var john = new Person("John", "Doe") { PhoneNumbers = new() { "1234567890" } };
var jane = john with { FirstName = "Jane" };
jane.PhoneNumbers.Add("0987654321");
// The list is modified only in the copy
Console.WriteLine(john.PhoneNumbers.Count); // 1
Console.WriteLine(jane.PhoneNumbers.Count); // 2
Do you have a question or a suggestion about this post? Contact me!