59

The C# 9 records feature specification includes the following:

A record type contains two copying members:

A constructor taking a single argument of the record type. It is referred to as a "copy constructor". A synthesized public parameterless instance "clone" method with a compiler-reserved name

But I cannot seem to call either of these two copying members:

public record R(int A);
// ...
var r2 = new R(r); // ERROR: inaccessible due to protection level
var r3 = r.Clone(); // ERROR: R does not contain a definition for Clone

From this, I understand that the constructor is protected and thus can't be accessed outside the record's inheritance hierarchy. And so we're left with code like this:

var r4 = r with { };

But what about cloning? The clone method is public according to the specification above. But what is its name? Or is it an effectively random string so that it should not be called outside the record's inheritance hierarchy? If so, what is the correct way to deep copy records? It seems from the specification that one is able to create one's own clone method. Is this so, and what would be an example of how it should work?

Sinatr
  • 20,892
  • 15
  • 90
  • 319
mbabramo
  • 2,573
  • 2
  • 20
  • 24
  • 3
    *"But what about cloning?"* That's what the `with` expression does. You're not supposed to know the name of the "clone" method. That's what "compiler-reserved" means. It's there to support `with` expressions. – madreflection Sep 21 '20 at 14:40
  • 5
    Implementation detail on the table: the name is `$`. By design, you can't call it yourself. – madreflection Sep 21 '20 at 14:47
  • @madreflection If the "with expression" does a deep clone (duplicating all records referred to by properties within the record), how would I do a shallow copy? If records were immutable, this wouldn't matter, but immutability is not guaranteed. – mbabramo Sep 21 '20 at 14:51
  • 2
    It does not. This cloning mechanism is a shallow copy. The document you linked doesn't contain the word "deep" anywhere. If you want a deep copy, you'll need to implement that yourself. – madreflection Sep 21 '20 at 14:53
  • I suggest taking a look at the `ICloneable` interface. With C# 9.0's covariant returns, it regains relevance. The `Clone` method can be made to return a more derived type without workarounds. Combine that with the protected cloning constructor and init-only properties. – madreflection Sep 21 '20 at 14:58
  • 1
    @madreflection Thank you. I think what confused me was the use of "copy" and "clone" in the documentation, but you are correct that the word "deep" is not used, so that must be implemented in some other way. I was also confused by the documentation's statement "If a virtual "clone" method is present in the base record, ..." As I understand it now, that would always be a synthetically created Clone() method, not a user-defined one; attempting to create such a method oneself generates an error. – mbabramo Sep 21 '20 at 15:11
  • A record can only derive from another record or `object`. That conditional statement is covering the latter since `object` won't have that method. – madreflection Sep 21 '20 at 15:15
  • 1
    It should bo noted imo, that 'shallow copy' is a well-defined concept, whereas 'deep copy' is not. – TaW Sep 21 '20 at 15:22

2 Answers2

67

But what about cloning?

var r4 = r with { };

performs a shallow clone on r.

The clone method is public according to the specification above. But what is its name?

The C# compiler has a fairly common trick where it gives generated members names which are illegal in C#, but legal in IL, so that they can't be called except from the compiler, even if they're public. In this case the name of the Clone method is <Clone>$.

If so, what is the correct way to deep copy records?

Deep copying you're out of luck. However since records should ideally be immutable, there should be no difference in practice between a shallow copy, a deep copy, and the original instance.

It seems from the specification that one is able to create one's own clone method. Is this so, and what would be an example of how it should work?

Unfortunately this didn't make the cut for C# 9, but there's a strong chance it'll be in C# 10.

Yair Halberstadt
  • 5,733
  • 28
  • 60
  • 4
    Is there a discussion on the custom `Clone` in C# 10 somewhere? It feels like introducing that will make it suffer from the same problems `ICloneable` always had, not knowing whether a `$` will do a shallow or - custom - deep clone. – Ray Aug 06 '21 at 08:39
  • It hasn't be discussed at all for C# 10. Maybe C# 11... – Yair Halberstadt Aug 07 '21 at 18:14
  • 1
    So they've introduced with expressions and they only work with records and can never be used on anything else? – John Jan 13 '22 at 12:33
  • For now, yes @John – Yair Halberstadt Jan 13 '22 at 12:56
  • If you want a deep copy you could I suppose create another method "DeepCopy()" that creates what you need. I found that the generated clone method can get circular, without that and it can lead to stack overflow errors. – Lamaan Ball Feb 07 '22 at 10:06
  • 2
    @John - `with` expressions now work for `struct` types as well as `record`. But **not** `class` types. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct#nondestructive-mutation – Mike Christiansen May 13 '22 at 14:20
11

To perform deep clone, you add your own copy constructor to a record:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public record EmployeeRecord(int UniqueId, Person Employee)
{

    // Custom copy constructor (Should be protected or private)
    protected EmployeeRecord(EmployeeRecord other)
    {
        UniqueId = other.UniqueId;

        // Do deep copy stuff
        Employee = new Person 
        { 
            FirstName = Employee.FirstName, 
            LastName = Employee.LastName 
        };
    }
}

The compiler will not generate its own in this case. Then you use the "with" keyword:

var emp1 = new EmployeeRecord(100, 
    new Person { FirstName = "John", LastName = "Doe" });

var emp2 = emp1 with { };
Mike Russo
  • 111
  • 1
  • 2