Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ignoring specific fields when using "with" on a C# 9 record?

When creating a new instance of a C# 9 record by using the with keyword, I'd like to ignore some fields instead of copying them into the new instance too.

In the following example, I have a Hash property. Because it is very expensive in computation, it is only computed when needed and then cached (I have a deeply immutable record, so that hash will never change for an instance).

public record MyRecord {

   // All truely immutable properties
   public int ThisAndManyMoreComplicatedProperties { get; init; } 
   // ...

   // Compute only when required, but then cache it
   public string Hash {
      get {
         if (hash == null)
            hash = ComputeHash();
         return hash;
      }
   }

   private string? hash = null;
}

When calling

MyRecord myRecord = ...;
var changedRecord = myRecord with { AnyProp = ... };

changedRecord contains the hash value from myRecord, but what I want to have is the default value null again.

Any chance to mark the hash field as "transient"/"internal"/"reallyprivate"..., or do I have to write my own copy-constructor to mimic this feature?

like image 377
Andi Avatar asked Feb 10 '21 11:02

Andi


2 Answers

I found a workaround: You can (ab)use inheritance to split the copy constructor in two parts: A manual one only for hash (in the base class) and an auto-generated one in the derived class copying all your valuable data fields.

This has the additional advantage of abstracting away your hash (non-)caching logic. Here's a minimal example (fiddle):

abstract record HashableRecord
{
    protected string hash;
    protected abstract string CalculateHash();
    
    public string Hash 
    {
        get
        {
            if (hash == null)
            {
                hash = CalculateHash(); // do expensive stuff here
                Console.WriteLine($"Calculating hash {hash}");
            }
            return hash;
        }
    }
    
    // Empty copy constructor, because we explicitly *don't* want
    // to copy hash.
    public HashableRecord(HashableRecord other) { }
}

record Data : HashableRecord
{
    public string Value1 { get; init; }
    public string Value2 { get; init; }

    protected override string CalculateHash() 
        => hash = Value1 + Value2; // do expensive stuff here
}

public static void Main()
{
    var a = new Data { Value1 = "A", Value2 = "A" };
    
    // outputs:
    // Calculating hash AA
    // AA
    Console.WriteLine(a.Hash);

    var b = a with { Value2 = "B" };
    
    // outputs:
    // AA
    // Calculating hash AB
    // AB
    Console.WriteLine(a.Hash);
    Console.WriteLine(b.Hash);
}
like image 75
Heinzi Avatar answered Sep 27 '22 17:09

Heinzi


I found a workaround for my problem. This does not solve the general problem, and it has another disadvantage: I have to cache the last state of the object, until the hash was recomputed. I understand this is a tradeoff between a potentially heavy computation and higher memory usage.

The trick is to remember the last object reference when the hash was computed. When calling the Hash property again, I check if meanwhile the object reference has been changed (i.e. if a new object was created).

public string Hash {
   get {
      if (hash == null || false == ReferenceEquals(this, hashRef)) {
         hash = ComputeHash();
         hashRef = this;
      }
      return hash;
   }
}
private string? hash = null;
private MyRecord? hashRef = null;

I'm still looking for a better solution.

EDIT: I recommend Heinzi's solution!

like image 34
Andi Avatar answered Sep 27 '22 16:09

Andi