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<person></person>消费数据、Actor 模型中作为消息体、或缓存层返回只读视图。 record 的
with表达式生成新实例,避免共享可变状态——
var adult = person with { Age = person.Age + 1 }; 是线程安全的
注意:如果 record 包含可变引用类型字段(如 public List<string> Tags { get; init; }</string>),外部仍可修改 Tags.Add(...),此时需手动冻结或用
IReadOnlyList<string></string>与
struct不同,record 是引用类型,但因不可变,其引用共享无副作用
混合使用:用 record 封装状态,用普通 class + Primary Constructor 封装行为与并发协调
一个健壮的并发数据模型往往需要“不可变数据 + 可控可变协调器”。例如,用
record OrderEvent(Guid Id, decimal Amount, DateTime Timestamp)表示事件,再用带 Primary Constructor 的
class OrderProcessor(int maxRetries)管理重试逻辑和状态机。
这种分层避免了把业务规则和数据契约混在同一类型里,也防止 record 因强行塞入锁或
ConcurrentStack而失去语义清晰性。 Primary Constructor 适合初始化协调器内部的配置参数(如超时、重试次数、
ConcurrentDictionary实例),而非业务数据本身 record 字段不应包含
ConcurrentQueue<t></t>或
AutoResetEvent等同步原语——那是协调器的责任 性能影响:record 的值相等性比较比引用比较略慢,但在消息路由、去重等场景中,语义正确性远比微秒级差异重要
public record OrderCreated(Guid OrderId, string Product, decimal Total);
public class OrderDispatcher(TimeSpan dispatchTimeout)
{
private readonly ConcurrentQueue<OrderCreated> _queue = new();
private readonly TimeSpan _timeout = dispatchTimeout;
<pre class='brush:php;toolbar:false;'>public void Enqueue(OrderCreated order) => _queue.Enqueue(order);
public bool TryDequeue([NotNullWhen(true)] out OrderCreated? order) =>
_queue.TryDequeue(out order);}
真正容易被忽略的是:record 的不可变性只作用于其直接声明的字段。一旦你嵌套了可变对象(比如
public Dictionary<string object> Metadata { get; init; }</string>),整个“不可变”承诺就失效了——并发下依然可能出错。别指望编译器替你检查深层可变性。 