Primary Constructors 不提供线程安全,仅简化构造语法;record 天然不可变,适合并发数据传递;二者应分层使用:record 封装不可变数据,带 Primary Constructor 的 class 封装可变协调逻辑。

Primary Constructors 不能直接用于并发安全的可变状态封装
Primary Constructors 是 C# 12 引入的语法糖,它让构造逻辑更紧凑,但本身不提供线程安全保证。如果你写 public class Counter(int initial = 0) { public int Value = initial; },Value 是公开字段,多个线程同时读写会引发竞态——编译器不会自动加锁或转为 volatile。
常见错误现象:在高并发计数场景中,counter.Value++ 看似原子,实则被拆成“读-改-写”三步,结果远小于预期值。
- Primary Constructor 仅影响构造函数签名和初始化顺序,不改变字段/属性的内存可见性或访问控制
- 若需并发安全,必须显式使用
Interlocked.Increment(ref counter.Value)或封装为private int _value; public int Value => Volatile.Read(ref _value); - 不要依赖 Primary Constructor “看起来简洁”就忽略同步原语——它不替代
lock、ConcurrentDictionary或不可变设计
record 默认不可变,天然适合做并发场景下的数据快照或消息载荷
用 record Person(string Name, int Age) 声明的类型,所有字段默认是 init(仅构造时可设),且自动生成值相等性(Equals/GetHashCode)。这使它成为跨线程传递数据的理想载体:无需担心被意外修改,也不必深拷贝。
典型使用场景:ASP.NET Core 中从 ConcurrentQueue 消费数据、Actor 模型中作为消息体、或缓存层返回只读视图。
- record 的
with表达式生成新实例,避免共享可变状态——var adult = person with { Age = person.Age + 1 };是线程安全的 - 注意:如果 record 包含可变引用类型字段(如
public List),外部仍可修改Tags { get; init; } Tags.Add(...),此时需手动冻结或用IReadOnlyList - 与
struct不同,record 是引用类型,但因不可变,其引用共享无副作用
混合使用:用 record 封装状态,用普通 class + Primary Constructor 封装行为与并发协调
一个健壮的并发数据模型往往需要“不可变数据 + 可控可变协调器”。例如,用 record OrderEvent(Guid Id, decimal Amount, DateTime Timestamp) 表示事件,再用带 Primary Constructor 的 class OrderProcessor(int maxRetries) 管理重试逻辑和状态机。
Sail企业网站管理系统(以下称Sail)是一个基于PHP+Mysql架构的企业网站管理系统。Sail 采用模块化方式开发,功能强大灵活易于扩展,并且完全开放源代码,面向大中型站点提供重量级企业网站建设解决方案。2年来,凭借Silence长期积累的丰富的Web开发及数据库经验和勇于创新追求完美的设计理念,使得Sail得到了很多公司和网站的认可,并且越来越多地被应用到大中型商业网站主要功能:单页、文
这种分层避免了把业务规则和数据契约混在同一类型里,也防止 record 因强行塞入锁或 ConcurrentStack 而失去语义清晰性。
- Primary Constructor 适合初始化协调器内部的配置参数(如超时、重试次数、
ConcurrentDictionary实例),而非业务数据本身 - record 字段不应包含
ConcurrentQueue或AutoResetEvent等同步原语——那是协调器的责任 - 性能影响:record 的值相等性比较比引用比较略慢,但在消息路由、去重等场景中,语义正确性远比微秒级差异重要
public record OrderCreated(Guid OrderId, string Product, decimal Total);
public class OrderDispatcher(TimeSpan dispatchTimeout)
{
private readonly ConcurrentQueue _queue = new();
private readonly TimeSpan _timeout = dispatchTimeout;
public void Enqueue(OrderCreated order) => _queue.Enqueue(order);
public bool TryDequeue([NotNullWhen(true)] out OrderCreated? order) =>
_queue.TryDequeue(out order);
}
真正容易被忽略的是:record 的不可变性只作用于其直接声明的字段。一旦你嵌套了可变对象(比如 public Dictionary Metadata { get; init; } ),整个“不可变”承诺就失效了——并发下依然可能出错。别指望编译器替你检查深层可变性。