线程安全构造与属性赋值:对比C#对象初始化器的实现差异
C#对象初始化器与线程安全构造的对比分析
咱们先从微软官方给出的这个对象初始化器示例说起:
class Cat { // Auto-implemented properties. public int Age { get; set; } public string Name { get; set; } } Cat cat = new Cat { Age = 10, Name = "Fluffy" };
官方文档说明:对象初始化器语法允许创建实例,之后将已完成属性赋值的新对象分配给赋值语句中的变量。
不过在多线程场景下,这种写法和传统的带参数构造函数相比,线程安全特性可是有不小的区别,咱们来拆解分析:
1. 对象初始化器的本质:构造后赋值
你可能没意识到,上面的对象初始化器代码,编译器其实会把它翻译成类似这样的逻辑:
Cat tempCat = new Cat(); // 先调用无参构造创建实例 tempCat.Age = 10; // 逐一给属性赋值 tempCat.Name = "Fluffy"; Cat cat = tempCat; // 最后把实例赋值给变量
这就带来了线程安全的隐患:如果在属性赋值的过程中,这个临时的tempCat引用意外泄露给了其他线程(比如构造函数里不小心把this传给了某个异步回调),那其他线程就可能拿到一个属性未完全赋值的半初始化对象——比如看到Age=10但Name=null的状态。
2. 线程安全的构造方式:带参数构造函数
如果咱们改用带参数的构造函数来实现:
class Cat { public int Age { get; } public string Name { get; } public Cat(int age, string name) { Age = age; Name = name; } } Cat cat = new Cat(10, "Fluffy");
根据C#的内存模型规则:对象的引用只有在构造函数完全执行完成后,才会被其他线程看到。也就是说,其他线程要么拿到一个完全初始化好的Cat实例,要么根本看不到这个对象的存在,完全避免了半初始化状态被访问的风险。
3. 特殊情况:Init-only属性的对象初始化器
在C# 9及以后,咱们可以用init修饰符定义只读属性,这时候用对象初始化器的写法是安全的:
class Cat { public int Age { get; init; } public string Name { get; init; } } Cat cat = new Cat { Age = 10, Name = "Fluffy" };
这是因为编译器会把init属性的初始化逻辑,直接整合到构造过程中,和带参数构造函数的线程安全特性一致——对象在构造完成前,引用不会泄露给其他线程,自然也就不存在半初始化的问题。
总结一下
- 普通可写属性的对象初始化器:本质是构造后逐属性赋值,多线程场景下可能暴露半初始化对象,存在线程安全风险;
- 带参数构造函数:符合C#内存模型的线程安全保证,是多线程环境下初始化对象的可靠方式;
- Init-only属性的对象初始化器:编译器优化为构造阶段赋值,具备和构造函数相同的线程安全性,兼顾了语法简洁性和安全性。
内容的提问来源于stack exchange,提问作者Andrew Hill
相关产品推荐
相关产品推荐

