在 C# 9 记录上使用 "with" 时忽略特定字段?
Ignoring specific fields when using "with" on a C# 9 record?
在使用 with
关键字创建 C# 9 record
的新实例时,我想忽略一些字段,而不是将它们也复制到新实例中。
在下面的例子中,我有一个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
包含来自 myRecord
的 hash
值,但我想要的是再次使用默认值 null
。
是否有机会将 hash
字段标记为“transient”/“internal”/“reallyprivate”...,或者我是否必须编写自己的复制构造函数来模仿此功能?
如果我的理解正确的话,您想使用现有 MyRecord 对象的某些属性创建一个新的 MyRecord 对象吗?
我认为按照这些思路应该可行:
MyRecord myRecord = ...;
var changedRecord = new MyRecord with { AnyProp = myRecord.AnyProp... };
如您所见,使用 sharplab.io decompilation the with
call is translated into <Clone>$()
method call which internally calls copy constructor generated by compiler, so you need to define your own copy constructor 可以防止 Hash
被调用。
也如 with
关键字 doc 所述:
If you need to customize the record copy semantics, explicitly declare a copy constructor with the desired behavior.
我认为允许这样做的唯一内置机制是“复制构造函数”。如 this post 中所述:
A record implicitly defines a protected “copy constructor” – a constructor that takes an existing record object and copies it field by field to the new one...
“复制构造函数”只是一个构造函数,它接收与记录类型相同的实例作为参数。如果您只是实现此构造函数,则可以覆盖 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
为空的消息。如果您评论复制构造函数,您会看到只有第一次调用打印 null。
所以我认为这可以解决您的问题。这种方法的缺点是您必须手动复制每个 属性 记录,这可能非常烦人。如果你的记录有很多属性,你可以使用反射来迭代然后只复制你想要的。您还可以定义自定义 Attribute
来标记忽略字段。但请记住,使用反射总是有处理开销。
我找到了解决问题的方法。这 不能 解决一般问题,它还有另一个缺点:我必须缓存对象的最后状态,直到重新计算哈希。我知道这是潜在的繁重计算和更高内存使用之间的权衡。
诀窍是在计算散列时记住最后一个对象引用。再次调用 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;
我仍在寻找更好的解决方案。
编辑:我推荐 !
我找到了一个解决方法:您可以(滥用)使用继承将复制构造函数分成两部分:手动的仅用于 hash
(在基础 class 中)和自动的在派生中生成一个 class 复制所有有价值的数据字段。
这还有一个额外的好处,就是可以抽象出您的散列(非)缓存逻辑。这是一个最小的例子 (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);
}
在使用 with
关键字创建 C# 9 record
的新实例时,我想忽略一些字段,而不是将它们也复制到新实例中。
在下面的例子中,我有一个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
包含来自 myRecord
的 hash
值,但我想要的是再次使用默认值 null
。
是否有机会将 hash
字段标记为“transient”/“internal”/“reallyprivate”...,或者我是否必须编写自己的复制构造函数来模仿此功能?
如果我的理解正确的话,您想使用现有 MyRecord 对象的某些属性创建一个新的 MyRecord 对象吗?
我认为按照这些思路应该可行:
MyRecord myRecord = ...;
var changedRecord = new MyRecord with { AnyProp = myRecord.AnyProp... };
如您所见,使用 sharplab.io decompilation the with
call is translated into <Clone>$()
method call which internally calls copy constructor generated by compiler, so you need to define your own copy constructor 可以防止 Hash
被调用。
也如 with
关键字 doc 所述:
If you need to customize the record copy semantics, explicitly declare a copy constructor with the desired behavior.
我认为允许这样做的唯一内置机制是“复制构造函数”。如 this post 中所述:
A record implicitly defines a protected “copy constructor” – a constructor that takes an existing record object and copies it field by field to the new one...
“复制构造函数”只是一个构造函数,它接收与记录类型相同的实例作为参数。如果您只是实现此构造函数,则可以覆盖 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
为空的消息。如果您评论复制构造函数,您会看到只有第一次调用打印 null。
所以我认为这可以解决您的问题。这种方法的缺点是您必须手动复制每个 属性 记录,这可能非常烦人。如果你的记录有很多属性,你可以使用反射来迭代然后只复制你想要的。您还可以定义自定义 Attribute
来标记忽略字段。但请记住,使用反射总是有处理开销。
我找到了解决问题的方法。这 不能 解决一般问题,它还有另一个缺点:我必须缓存对象的最后状态,直到重新计算哈希。我知道这是潜在的繁重计算和更高内存使用之间的权衡。
诀窍是在计算散列时记住最后一个对象引用。再次调用 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;
我仍在寻找更好的解决方案。
编辑:我推荐
我找到了一个解决方法:您可以(滥用)使用继承将复制构造函数分成两部分:手动的仅用于 hash
(在基础 class 中)和自动的在派生中生成一个 class 复制所有有价值的数据字段。
这还有一个额外的好处,就是可以抽象出您的散列(非)缓存逻辑。这是一个最小的例子 (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);
}