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?
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);
}
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With