Customizing the behavior of record copy constructors

 
 
  • Gérald Barré

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.

C#
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:

C#
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:

C#
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:

C#
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!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub