Analysis by @stephentoub
You can easily see that by running this code on .NET Framework today:
using System;
using System.Reflection;
class Program
{
private static readonly int s_readonlyValue;
private static int s_value;
private static void Main()
{
Console.WriteLine(nameof(s_value) + ":");
for (int i = 0; i < 10; i++)
{
typeof(Program).GetField(nameof(s_value), BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, s_value + 1);
Console.WriteLine(s_value);
}
Console.WriteLine();
Console.WriteLine(nameof(s_readonlyValue) + ":");
for (int i = 0; i < 10; i++)
{
typeof(Program).GetField(nameof(s_readonlyValue), BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, s_readonlyValue + 1);
Console.WriteLine(s_readonlyValue);
}
}
}
This prints:
s_value:
1
2
3
4
5
6
7
8
9
10
s_readonlyValue:
0
0
0
0
0
0
0
0
0
0
because the JIT bakes the value of s_readonlyValue into the call site when it compiles it, and thus isn’t affected by subsequent changes to the field.