如何解决在 C# 9 记录上使用“with”时忽略特定字段?
在使用 record
关键字创建 C# 9 with
的新实例时,我想忽略某些字段,而不是将它们也复制到新实例中。
在以下示例中,我有一个 Hash
属性。因为它在计算上非常昂贵,所以只在需要时才计算然后缓存(我有一个深度不可变的记录,所以对于一个实例来说哈希永远不会改变)。
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;
}
打电话时
MyRecord myRecord = ...;
var changedRecord = myRecord with { AnyProp = ... };
changedRecord
包含来自 hash
的 myRecord
值,但我想要的是再次使用默认值 null
。
是否有机会将 hash
字段标记为“transient”/“internal”/“reallyprivate”...,或者我是否必须编写自己的复制构造函数来模仿此功能?
解决方法
我找到了解决我的问题的方法。这不会解决一般问题,而且还有另一个缺点:我必须缓存对象的最后状态,直到重新计算散列。我知道这是潜在的繁重计算和更高的内存使用量之间的权衡。
诀窍是在计算散列时记住最后一个对象引用。再次调用 Hash
属性时,我检查对象引用是否已同时更改(即是否创建了新对象)。
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;
我仍在寻找更好的解决方案。
编辑:我推荐Heinzi's solution!
,我找到了一种解决方法:您可以(ab)使用继承将复制构造函数拆分为两部分:仅用于 hash
的手动部分(在基类中)和自动生成的部分在派生类中复制所有有价值的数据字段。
这还有一个额外的好处,就是抽象掉你的散列(非)缓存逻辑。这是一个最小的示例 (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);
}
,
正如你看到的 sharplab.io 反编译,with
调用被翻译成 <Clone>$()
方法调用,它内部调用编译器生成的复制构造函数,所以你需要定义自己的 {{3 }} 以防止调用 Hash
。
同样如 with
关键字 copy constructor 所述:
如果您需要自定义记录复制语义,请显式声明具有所需行为的复制构造函数。
,我认为唯一允许这样做的内置机制是“复制构造函数”。如 this post 中所述:
一条记录隐含地定义了一个受保护的“复制构造函数”——一个构造函数,它接受一个现有的记录对象,并将它逐个字段地复制到新的......
“复制构造函数”只是一个构造函数,它接收一个相同类型的记录实例作为参数。如果您只是实现此构造函数,则可以覆盖 with
表达式的默认行为。我已经根据您的代码进行了测试,这是记录声明:
public record MyRecord
{
protected MyRecord(MyRecord original)
{
ThisAndMayMoreComplicatedProperties = original.ThisAndMayMoreComplicatedProperties;
hash = null;
}
public int ThisAndMayMoreComplicatedProperties { get; init; }
string? hash = null;
public string Hash
{
get
{
if (hash is null)
{
Console.WriteLine("The stored hash is currently null.");
}
return hash ??= ComputeHash();
}
}
string ComputeHash() => "".PadLeft(100,'A');
}
请注意,当我调用属性 getter 时,我会检查 hash
是否为空并打印一条消息。然后我做了一个小程序来检查:
var record = new MyRecord { ThisAndMayMoreComplicatedProperties = 100 };
Console.WriteLine($"{record.Hash}");
var newRecord = record with { ThisAndMayMoreComplicatedProperties = 200 };
Console.WriteLine($"{newRecord.Hash}");
如果您运行此程序,您会注意到对 Hash
的两次调用都将打印私有 hash
为空的消息。如果您注释复制构造函数,您将看到只有第一次调用打印空值。
所以我认为这可以解决您的问题。这种方法的缺点是您必须手动复制记录的每个属性,这可能非常烦人。如果你记录有很多属性,你可以使用反射来迭代然后只复制你想要的。您还可以定义自定义 Attribute
来标记忽略字段。但请记住,使用反射总是有处理开销。
如果我对您的理解正确,您想使用现有 MyRecord 对象的某些属性创建一个新的 MyRecord 对象?
我认为应该按照以下方式行事:
MyRecord myRecord = ...;
var changedRecord = new MyRecord with { AnyProp = myRecord.AnyProp... };
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。