C#记录类型中意外的数据不一致问题解析

news/2025/11/10 6:04:58/文章来源:https://www.cnblogs.com/qife122/p/19205700

意外的记录不一致性

前几天,我在排查代码中的一个bug时,发现这源于我对C#记录类型工作方式的误解。可能只有我一个人期望它们以我理解的方式工作,但我觉得值得写下来分享。

这个问题是我在修改2029年英国大选网站时发现的,但它实际上与大选无关,因此我没有将其包含在大选网站的博客系列中。

回顾:非破坏性变更

当记录类型被引入C#时,同时引入了使用with操作符的"非破坏性变更"概念。其思想是记录类型可以是不可变的,但你可以轻松高效地创建一个新实例,该实例具有与现有实例相同的数据,但具有一些不同的属性值。

例如,假设你有这样一个记录:

public sealed record HighScoreEntry(string PlayerName, int Score, int Level);

然后你可以编写以下代码:

HighScoreEntry entry = new("Jon", 5000, 50);
var updatedEntry = entry with { Score = 6000, Level = 55 };

这不会改变第一个实例中的数据(因此entry.Score仍为5000)。

回顾派生数据

记录类型不允许为主要构造函数指定构造函数体(这是我在早期关于记录和集合的文章中打算写的内容),但你可以基于主要构造函数中的参数值初始化字段(以及自动实现的属性)。

作为一个非常简单(且高度人为设计)的示例,你可以创建一个记录,在初始化时确定值是奇数还是偶数:

public sealed record Number(int Value)
{public bool Even { get; } = (Value & 1) == 0;
}

乍一看,这看起来不错:

var n2 = new Number(2);
var n3 = new Number(3);
Console.WriteLine(n2); // 输出: Number { Value = 2, Even = True }
Console.WriteLine(n3); // 输出: Number { Value = 3, Even = False }

到目前为止一切顺利。直到本周,我一直认为这一切都很好。

问题:混合使用with和派生数据

问题出现在混合使用这两个功能时。如果我们更改上面的代码(同时保持记录本身不变),使用with操作符而不是调用构造函数来创建第二个Number,输出就会变得不正确:

var n2 = new Number(2);
var n3 = n2 with { Value = 3 };
Console.WriteLine(n2); // 输出: Number { Value = 2, Even = True }
Console.WriteLine(n3); // 输出: Number { Value = 3, Even = True }

"Value = 3, Even = True"真的不好。

这是怎么发生的?出于某种原因,我一直假设with操作符使用新值调用构造函数。但实际上并非如此。上面的with操作符大致转换为如下代码:

// 这不会编译,但大致是生成的内容。
var n3 = n2.<Clone>$();
n3.Value = 3;

<Clone>$方法(至少在这种情况下)调用生成的复制构造函数(Number(Number)),该构造函数复制ValueEven的支持字段。

这一切都有文档记录——但目前没有任何关于可能引入的不一致性的警告。(我将给微软的同事发邮件,看看能否在其中添加一些内容。)

请注意,由于Value是在克隆操作之后设置的,我们无论如何都无法编写一个复制构造函数来正确处理这个问题。(至少,没有直接的方法——我稍后会提到一个复杂的方法。)

如果有人想"为什么不直接使用计算属性?",显然这是可行的:

public sealed record Number(int Value)
{public bool Even => (Value & 1) == 0;
}

任何可以像这样轻松按需计算的属性都很棒——不仅不会出现本文中的问题,而且在内存方面也更高效。但这对于我在大选网站中使用的大多数记录中的属性来说真的不起作用,这些记录通常使用按ID索引的集合构建,或者执行其他相对昂贵的计算。

我们能做什么?

到目前为止,我想到了四种前进的方法,但没有一种是令人愉快的。我很想听听其他人的建议。

选项1:耸耸肩继续生活

现在我知道了这个问题,我可以避免在除了"简单"记录之外的任何东西上使用with操作符。如果没有计算属性或字段,with操作符仍然非常有用。

当然,存在一种风险,我可能在一个最初"简单"的记录类型上使用with操作符,然后后来引入一个计算成员。嗯。

选项2:编写Roslyn分析器来检测问题

至少在理论上,对于任何在声明它们的同一解决方案中使用的记录(这对我的大选网站来说是一切),编写一个Roslyn分析器应该是可行的,该分析器:

  • 分析每个声明记录中的每个成员初始化器,以查看使用了哪些参数
  • 分析每个with操作符的使用,以查看正在设置哪些参数
  • 如果两者之间存在任何交集,则记录错误

这很有吸引力,可能对其他人也有用。它的缺点是需要实现Roslyn分析器。我很久没有编写分析器了,但我猜这仍然是一个相当复杂的过程。如果我真正找到时间,这很可能是我会做的事情——但我希望有人评论说分析器已经存在,或者解释为什么不需要它。

更新,2025-07-29:我写了一对分析器!请参阅我的后续文章了解更多细节。

选项3:找出安全使用with的方法

我一直在尝试如何使用Lazy<T>来延迟计算任何属性,直到它们第一次被使用,这将在with操作符为属性设置新值之后。我想出了下面的模式——我认为这是有效的,但非常混乱。采用这种模式不需要将父记录中的每个新参数都反映在嵌套类型中——仅用于计算属性中使用的参数。

public sealed record Number(int Value)
{private readonly Lazy<ComputedMembers> computed =new(() => new(Value), LazyThreadSafetyMode.ExecutionAndPublication);public bool Even => computed.Value.Even;private Number(Number other){Value = other.Value;// 延迟创建ComputedMembers实例,直到computed = new(() => new(this), LazyThreadSafetyMode.ExecutionAndPublication);}// 这是一个结构体(或者可以是一个类),而不是记录,// 以避免为Value创建字段。我们只需要计算属性。// (我们甚至不需要使用主要// 构造函数,在某些情况下最好不要使用。)private struct ComputedMembers(int Value){internal ComputedMembers(Number parent) : this(parent.Value){}public bool Even { get; } = (Value & 1) == 0;}
}

这是:

  • 痛苦地记住要做
  • 开始时需要大量额外的代码(尽管设置完成后,添加新的计算成员并不太糟糕)
  • 由于添加了Lazy<T>实例,在内存方面效率低下

在"大型"记录中,这种低效可能无关紧要,但它使得在只有几个参数的"小型"记录中使用计算属性变得痛苦,特别是如果这些只是数字等。

选项4:请求更改语言

我提出这一点只是为了完整性。我非常信任C#设计团队:他们是聪明的人,会非常仔细地思考问题。如果我是第一个提出这个"问题"的人,我会感到震惊。我认为更有可能的是,在确定当前行为是最不坏的选择之前,已经详细讨论了这种行为的优缺点,并讨论和原型化了替代方案。

现在也许Roslyn编译器可以开始发出警告(选项2),这样我就不必编写分析器——也许可以为以后的C#版本添加替代方案(理想情况下为记录中的初始化提供更多灵活性,例如一个特殊命名的成员,在实例"准备就绪"时调用,并且仍然可以写入只读属性)……但如果没有明确的鼓励,我可能不会开始为此创建提案。

结论

我很少在C#中发现陷阱,但这对我来说确实像一个陷阱。也许只是因为我在大选网站中如此广泛地使用了计算属性——也许记录真的不是设计用来这样使用的,我的一半记录类型真的应该是类。

我不想停止使用记录,我也绝对不鼓励其他人这样做。我不想停止使用with操作符,再次强调,我也不鼓励其他人这样做。我希望这篇文章能对那些以不安全的方式使用with的人起到一点警示作用。

哦,当然,如果我确实编写了一个能够检测到这一点的Roslyn分析器,我会编辑这篇文章以链接到它。(如前所述,这就是那篇文章。)
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)

公众号二维码

公众号二维码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/960887.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java 中 double 的精度问题,以及为什么 BigDecimal 没有这个问题

Java 中 double 的精度问题,以及为什么 BigDecimal 没有这个问题Java 中 double 的精度问题,以及为什么 BigDecimal 没有这个问题 当你在 Java 中写出: System.out.println(0.1 + 0.2); // 0.30000000000000004是不…

AI元人文:构建有界可信的人机文明新范式

AI元人文:构建有界可信的人机文明新范式 摘要 AI元人文构想提出了一种全新的人机协作范式,通过建立"环境-价值-规则"的动态框架,构建"权重调适"与"划界"两大核心机制,实现人工智能在…

synchronized` 的“锁升级/路径解析

synchronized` 的“锁升级/路径解析synchronized` 的“锁升级/路径 为什么会升级?什么时候升级?升级后发生了什么?该怎么写代码更高效?0. 先给一个总图(JDK 17+ 的主线)无锁 → 轻量级锁(用户态 CAS + 自旋) →…

synchronized` 的“锁升级/路径

synchronized` 的“锁升级/路径synchronized` 的“锁升级/路径 它是 HotSpot 为了在不同竞争程度下取得最好性能而设计的多形态锁。常见演进(JDK8 语境)是:无锁 → 偏向锁(Biased) → 轻量级锁(Thin/Lightweight…

HEAD^n和HEAD~n的区别

https://www.delftstack.com/zh/howto/git/git-3-head/ 如果当前只有一个分支(使用rebase而非merge),则HEAD^n和HEAD~n没有任何区别。 HEAD、HEADn HEAD~n是沿当前分支往前n个提交,HEAD~1为前一个提交,可以省略为HE…

CountDownLatch 与 CyclicBarrier 区别深度解析

CountDownLatch 与 CyclicBarrier 区别深度解析CountDownLatch 与 CyclicBarrier 深度解析:并发编程中的“闸门”与“集合点” 在 Java 并发工具包 java.util.concurrent 中,CountDownLatch 与 CyclicBarrier 是最常…

【比赛游记】2025 ICPC 南京站游记

希望能带来好运。Day -1 坐飞机飞往南京。一叶知秋,凉风徐来。南京秋意渐浓。 南京人喜欢吃鸭子,恰恰好好我也喜欢吃鸭子。这下有得吃了!强烈推荐鸭血粉丝汤、盐水鸭、板鸭 ... Day 0 早上九点出发前往南京航空航天…

变量和简单的数据类型

一.变量、字符串、print、类型转换 message = "Hello Python world" #字符串变量 print(message) print("apple"+str(4)) print(1.4+int("1"))

Not physics

People(Letters) maybe dont fear Wars as long as they can win. As long as they can totally eliminate an Ethnicity, just like to Indians. they maybe want to did this to WW2-Germany, but Germans are not I…

为啥ls -d */列出所有目录

ls -F会在目录名后面加/:-F, --classify[=WHEN] append indicator (one of */=>@|) to entries WHEN 但ls -d */是因为bash的expansion. 我们可以写个 ls.sh,其 echo $* 来证实这一点。 我们还可以如下,sadly我解…

我的旮旯回忆录

好吧好吧,写这个东西的原因就是。我在 au 之后接受过一定的采访,然后大家最喜欢问的一个问题是:小 OU 酱你玩不玩游戏呀?? 每次都是这样支支吾吾的糊弄过去。 好吧,很没有意思对吧。 不过为了满足大家的好奇心,…

2025年11月AI搜索营销推荐全览:五强格局趋势与实操

2025年11月,生成式引擎优化进入“多平台算法并行”阶段,企业主普遍面临“预算有限、平台分散、效果难衡量”的三重焦虑。本文以“五强格局”为切口,用可验证的公开信息,把技术路线、服务差异、落地场景、潜在风险拆…

为啥ls -d */能列出所有目录

ls -F会在目录名后面加/:-F, --classify[=WHEN] append indicator (one of */=>@|) to entries WHEN 但ls -d */是因为bash的expansion. 我们可以写个 ls.sh,其 echo $* 或 t="*/"; echo $t 来证实这一点…

2025年11月AI搜索营销推荐指南:五强全景对比助决策

2025年11月,当品牌主们发现传统SEO流量曲线越来越平缓,而豆包、DeepSeek、通义千问等AI对话入口的日活却突破亿级时,“AI搜索营销”四个字就不再是概念,而是必须立刻行动的战场。过去三个月,我们收到超过两百家企…

2025年11月AI搜索优化推荐榜:从诊断到落地的完整路径

在生成式AI全面改写搜索入口的2025年,企业市场部普遍面临同一道难题:当用户不再打开传统搜索引擎,而是在DeepSeek、豆包、通义千问、元宝、Kimi等对话式入口直接提问时,品牌信息如何被算法选中并优先呈现?这一痛点…

2025年11月AI搜索优化推荐榜:五强数据表现与落地案例对照

2025年11月,生成式引擎优化进入规模化落地期,企业面临“平台碎片化、算法迭代快、效果可验证”三大痛点。本文以“随机规则21”为结构框架,先抛出典型疑问,再给出五强榜单,继而用可公开验证的数据与场景描述,帮助…

2025年11月deepseek关键词排名优化推荐:五家优选机构对比助您高效落地

开场白 “想让品牌在DeepSeek里一搜就靠前,却担心选错服务商?”这是多数市场负责人在2025年四季度最焦虑的问题。AI搜索流量已被验证为成本最低的精准入口,但各家GEO机构技术路线差异大,报价跨度从季度几万到几十万…

2025年11月deepseek关键词排名优化推荐:五强榜单一文看懂选型

2025年11月,当企业试图在DeepSeek等生成式引擎中锁定关键词排名时,往往面临“平台算法差异大、优化策略碎片化、效果评估缺标准”三重痛点。本文以用户真实决策场景为轴,随机调用结构规则36,用可公开验证的数据与一…

2025年11月GEO品牌推荐:技术引擎驱动跨平台协同增长

如果把“让品牌在AI搜索里被看见”当成一场接力赛,企业主最常遇到的痛点是:平台规则更新比计划快、内部团队缺乏跨平台经验、预算有限却想全域覆盖。2025年11月的这份榜单,正是站在“预算、人力、时间”三条红线交叉…