.NET 4.5中string.Empty(或System.String::Empty)的行为已更改

42

简短版:

C#代码

typeof(string).GetField("Empty").SetValue(null, "Hello world!");
Console.WriteLine(string.Empty);

当编译并运行时,在.NET版本4.0及更早版本下会输出"Hello world!",但在.NET 4.5和.NET 4.5.1下会输出""。如何忽略对字段的写入或是谁重置了该字段?
更详细的版本: 我从未真正理解为什么string.Empty字段(也称为[mscorlib]System.String::Empty)不是const(又称为literal),请参见“Why isn't String.Empty a constant?”。这意味着,例如在C#中,我们无法在以下情况下使用string.Empty
  • 在形式为case string.Empty:switch语句中
  • 作为可选参数的默认值,例如void M(string x = string.Empty) { }
  • 应用属性时,例如[SomeAttribute(string.Empty)]
  • 其他需要编译时常量的情况

这对于关于是否使用 string.Empty"" 的“宗教战争”有着重要的影响,参见“在C#中,我应该使用string.Empty还是String.Empty还是""来初始化字符串?”。

几年前,我通过反射将Empty设置为其他字符串实例,并观察了BCL的许多部分因此而表现出奇怪的行为。这相当多。并且更改Empty引用似乎持续了整个应用程序的生命周期。现在,前几天我试图重复那个小把戏,但是使用.NET 4.5机器,我再也无法做到了。

(注意!如果您的计算机上安装了.NET 4.5,则您的PowerShell仍然使用旧版本的.NET(编辑:仅适用于未更新到PowerShell 2.0以外的Windows 7或更早版本),因此请尝试将[String].GetField("Empty").SetValue($null, "Hello world!")复制粘贴到PowerShell中以查看更改此引用的一些效果。)

当我试图寻找原因时,我偶然发现了一个有趣的线程 ".NET 4.5 beta中FatalExecutionEngineError的原因是什么?"。在那个问题的被接受的答案中,指出到通过版本4.0,System.String有一个静态构造函数.cctor,其中设置了Empty字段(在C#源代码中,这可能只是一个字段初始化器),而在4.5中没有静态构造函数存在。在两个版本中,该字段本身看起来都是相同的:
.field public static initonly string Empty

(如在IL DASM中看到的那样)。

除了String::Empty之外,似乎没有其他字段受到影响。例如,我尝试使用System.Diagnostics.Debugger::DefaultCategory进行实验。这种情况似乎是类似的:一个包含static readonlystatic initonly)类型为string的字段的密封类。但在这种情况下,通过反射更改该值(引用)似乎可以正常工作。

回到问题:

从技术上讲,为什么在设置该字段时Empty在4.5中似乎没有发生变化?我已经验证了C#编译器不会“欺骗”读取,它输出的IL如下:

ldsfld     string [mscorlib]System.String::Empty

所以实际字段应该被读取。


在我的问题被悬赏之后进行编辑: 请注意,写操作(需要反射,因为该字段是readonly(即IL中的initonly))实际上按预期工作。 异常的是读取操作。如果使用反射进行读取,例如typeof(string).GetField("Empty").GetValue(null),则一切正常(即可以看到值的更改)。请参见下面的评论。

因此,更好的问题是:为什么这个新版本的框架在读取这个特定字段时会欺骗呢?


6
编译器没有欺骗的可能,但是公共语言运行库(CLR)有相当大的欺骗可能性。 - dlev
4
当您立即使用反射打印出string.Empty的值时会发生什么? - Kirk Woll
2
@KirkWoll 太棒了!我不知道为什么我没想到这样做。当我也使用反射进行读取时,新值得到了尊重。这意味着 Console.WriteLine(typeof(string).GetField("Empty").GetValue(null)); 在4.5中会打印出 "Hello world!" - Jeppe Stig Nielsen
6
@Renan,并不是更适合程序员。 OP提出了一个具体可回答的问题,原则上只有一个正确答案。 Stack Overflow非常适合这种情况。 - Kirk Woll
1
@BenVoigt: 那个问题几乎与这个问题没有任何关联;唯一的松散联系就是“使用反射访问string.Empty”。另一个问题是关于为什么可以在系统程序集中写入readonly字段;而这个问题则是关于为什么和如何在某些版本的.NET中,特定字段会拒绝某些基于反射的写操作。 - O. R. Mapper
显示剩余7条评论
3个回答

22
新版.NET的JIT有所不同,它通过内联到一个特定的String实例来优化对String.Empty的引用,而不是加载Empty字段中存储的值。这是在ECMA-335 Partition I §8.6.1.2中“只读属性约束”的定义下进行的,该定义可能被解释为意味着在String类初始化后,String.Empty字段的值将不会更改。

此外,在.NET 4中,System.String类有一个静态构造函数(方法.cctor()),该函数分配给字段。在.NET 4.5及更高版本中,不再是这种情况。我想知道为什么通过反射进行读取(似乎没有“作弊”)时,字段不会被视为null - Jeppe Stig Nielsen

3
我没有确切的答案,只能提供一些线索。
我看到String::EmptySystem.Diagnostics.Debugger::DefaultCategory之间唯一的区别在于前者带有__DynamicallyInvokableAttribute这个标记。
我不知道这个未经记录的属性的含义。有人在SO上问了关于这个属性的问题:What is the __DynamicallyInvokable attribute for? 我只能猜想这个属性被运行时捕获以执行某些缓存操作?

2
因为它有用。
这些系统定义的initonly字段的值是.NET运行时的全局不变量。如果这些不变量被破坏,那么关于行为就再也没有任何保证了。
在C++中,我们可能会规定这种情况会导致未定义行为。在.NET中,也是未定义行为,只是因为没有规则说明System.String.Empty.Length > 0时会发生什么。整个.NET和C#的所有层面的规范都描述了当System.String.Empty.Length == 0并且许多其他不变量都成立时的行为。
有关在不同运行时之间会有哪些优化以及其影响的更多信息,请参见以下问题的答案:

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接