示例
- 我们先来看一段结构体的代码 (基于 VS2022 + .NET 8.0)
public struct MyStruct(int number){ public int Number = number; public void SetNumber(int number) => Number = number;}public class Program{ private static MyStruct myStruct = new(1); public static void Main() { int before = myStruct.Number; myStruct.SetNumber(2); int after = myStruct.Number; Console.WriteLine($"before: {before}"); Console.WriteLine($" after: {after}"); Console.ReadKey(); }}
输出如下:
before: 1
?after: 2
修改为只读
private static readonly MyStruct myStruct = new(1);
输出如下:
before: 1
?after: 1
- 我们看到,修改只读结构体成员的字段失败了,但是编译器竟然没有报错
- 如果我们直接操作
myStruct.Number = 2; 编译器是会报错的,但是加了一个方法间接的修改,编译器就歇菜了
内部原理
我们查看反汇编代码,可以看到,在实际操作只读结构体成员字段的时候,会把该字段的值拷贝一份到一个新的堆栈变量上,然后再基于拷贝后的这个变量计算
17: myStruct.SetNumber(2); mov rcx,7FF9BD68E500h mov edx,9 call CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE (07FFA1D15B6F0h) mov rcx,26AE19DB1D0h //rcx保存结构体Number的地址 mov rcx,qword ptr [rcx] //拷贝Number的值到rcx mov qword ptr [rbp+70h],rcx //rcx的值赋值到临时变量 lea rcx,[rbp+70h] mov edx,2 call ConsoleTest_NET_8.MyStruct.SetNumber(Int32) (07FF9BD6A2BC8h)
导致的问题
- 我们先来看一段自旋锁的代码,基于 SpinLock
public class Program{ private static readonly SpinLock spinLock = new(false); public static void Main() { int sum = 0; Parallel.For(0, ushort.MaxValue, i => { bool lockToken = false; try { spinLock.Enter(ref lockToken); sum++; } finally { if (lockToken) { spinLock.Exit(); } } }); Console.WriteLine(sum); Console.ReadKey(); }}
- 我们期望的输出是: 65535, 但实际不是,因为隐藏的只读机制导致了字段值的拷贝, 这就造成了隐藏的 BUG
结论
-
警惕把结构体成员变量设置只读
-
在确定结构体内的字段只读时,可以使用 readonly 直接修饰 结构体本身或者字段,比如
public readonly struct MyStruct(int number){ public readonly int Number = number;}