当前位置: 首页 > news >正文

为什么 C# 的 string.Empty 是一个静态只读字段,而不是一个常量呢?

使用 C# 语言编写字符串常量的时候,你可能会发现可以使用 "" 而不能使用 string.Empty。进一步可以发现 string.Empty 实际上是一个静态只读字段,而不是一个常量。

为什么这个看起来最适合是常量的 string.Empty,竟然使用静态只读字段呢?


本文内容

    • string.Empty
    • Intrinsic 特性
    • JIT 编译器
    • 总结:为什么 string.Empty 需要是一个静态只读字段而不是常量?
    • 本文引申的其他问题
      • 能否反射修改 string.Empty 的值?
      • `""` 和 `string.Empty` 到底有什么区别?

string.Empty

这个问题,我们需要去看 .NET Core 的源码(当然 .NET Framework 也是一样的)。

[Intrinsic]
public static readonly string Empty;

值得注意的是上面的 Intrinsic 特性。

Intrinsic 特性

Intrinsic 特性的注释是这样的:

Calls to methods or references to fields marked with this attribute may be replaced at some call sites with jit intrinsic expansions.
Types marked with this attribute may be specially treated by the runtime/compiler.

翻译过来是:对具有此 Intrinsic 特性标记的字段的方法或引用的调用可以在某些具有 JIT 内部扩展的调用点处替换,标记有此属性的类型可能被运行时或编译器特殊处理。

也就是说,string.Empty 字段并不是一个普通的字段,对它的调用会被特殊处理。但是是如何特殊处理呢?

JIT 编译器

string.Empty 的注释是这样描述的:

The Empty constant holds the empty string value. It is initialized by the EE during startup. It is treated as intrinsic by the JIT as so the static constructor would never run. Leaving it uninitialized would confuse debuggers.
We need to call the String constructor so that the compiler doesn’t mark this as a literal. Marking this as a literal would mean that it doesn’t show up as a field which we can access from native.

翻译过来是:

Empty 常量保存的是空字符串的值,它在启动期间由执行引擎初始化。它被 JIT 视为内在的,因此静态构造函数永远不会运行。将它保持为未初始化的状态将会使得调试器难以解释此行为。
于是我们需要调用 String 的构造函数,以便编译器不会将其标记为文字。将其标记为文字将意味着它不会显示为我们可以从本机代码访问的字段。

说明一下:

  1. 注释里的 EE 是 Execution Engine 的缩写,其实也就是 CLR 运行时。
  2. 那个 literal 我翻译成了文字。实际上这里说的是 IL 调用字符串时的一些区别:
    • 在调用 "" 时使用的 IL 是 ldstr ""(Load String Literal)
    • 而在调用 string.Empty 时使用的 IL 是 ldsfld string [mscorlib]System.String::Empty(Load Static Field)
  3. 虽然 IL 在调用 ""string.Empty 时生成的 IL 不同,但是在 JIT 编译成本机代码的时候,生成的代码完全一样。
    • 详情请参见:.net - What’s the different between ldsfld and ldstr in IL? - Stack Overflow
    • 我写过一篇文章 .NET/C# 编译期间能确定的相同字符串,在运行期间是相同的实例 - 吕毅。虽然一般情况下取字符串常量实例的时候会去字符串池,但是不用担心取 "" 会造成性能问题,因为实际上 JIT 编译器已经特殊处理了,不会去找池子。

string.Empty 字段在整个 String 类型中你都看不到初始化的代码,String 类的静态构造函数也不会执行。也就是说,String 类中的所有静态成员都不会被托管代码初始化。String 的静态初始化过程都是由 CLR 运行时进行的,而这部分的初始化是本机代码实现的。

那本机代码又是如何初始化 String 类型的呢?在 CLR 运行时的 AppDomain::SetupSharedStatics() 方法中实现,可前往 GitHub 阅读这部分的源码:

  • coreclr/appdomain.cpp at ef1e2ab328087c61a6878c1e84f4fc5d710aebce · dotnet/coreclr
// This is a convenient place to initialize String.Empty.
// It is treated as intrinsic by the JIT as so the static constructor would never run.
// Leaving it uninitialized would confuse debuggers.

// String should not have any static constructors.
_ASSERTE(g_pStringClass->IsClassPreInited());

FieldDesc * pEmptyStringFD = MscorlibBinder::GetField(FIELD__STRING__EMPTY);
OBJECTREF* pEmptyStringHandle = (OBJECTREF*)
    ((TADDR)pLocalModule->GetPrecomputedGCStaticsBasePointer()+pEmptyStringFD->GetOffset());
SetObjectReference( pEmptyStringHandle, StringObject::GetEmptyString(), this );

总结:为什么 string.Empty 需要是一个静态只读字段而不是常量?

从上文中 string.Empty 的注释描述中可以知道:

  1. 编译器会将 C# 语言编译成中间语言 MSIL;
  2. 如果这是一个常量,那么编译器在不做特殊处理的情况下,就会生成 ldstr "",而这种方式不会调用到 String 类的构造函数(注意不是静态构造函数,String 类的静态构造函数是特殊处理不会调用的);
  3. 而如果这是一个静态字段,那么编译器可以在不做特殊处理的情况下,生成 ldsfld string [mscorlib]System.String::Empty,这在首次执行时会触发 String 类的构造函数,并在本机代码(非托管代码)中完成初始化。

当然,事实上编译器也可以针对此场景做特殊处理,但为什么不是在编译这一层进行特殊处理,我已经找不到出处了。

本文引申的其他问题

能否反射修改 string.Empty 的值?

不行!

实际上,在 .NET Framework 4.0 及以前是可以反射修改其值的,这会造成相当多的基础组件不能正常工作,在 .NET Framework 4.5 和以后的版本,以及 .NET Core 中,CLR 运行时已经不允许你做出这么出格儿的事了。

不过,如果你使用不安全代码(unsafe)来修改这个字段的值就当我没说。关于使用不安全代码转换字符串的方法可以参见:

  • C# 字符串首字符大写 - 林德熙
  • .NET/C# 编译期间能确定的相同字符串,在运行期间是相同的实例 - 吕毅

""string.Empty 到底有什么区别?

从前文你可以得知,在运行时级别,这两者 没有任何区别

于是,当你需要一个代表 “空字符串” 含义的时候,使用 string.Empty;而当你必须要一个常量时,就使用 ""


参考资料

  • String.CoreCLR.cs
  • Intrinsic
  • 在C#中 String.Empty和 “” 有什么区别? - 知乎
  • .net - What’s the different between ldsfld and ldstr in IL? - Stack Overflow

我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

相关文章:

  • 透明度叠加算法:如何计算半透明像素叠加到另一个像素上的实际可见像素值(附 WPF 和 HLSL 的实现)
  • C#/.NET 调试的时候显示自定义的调试信息(DebuggerDisplay 和 DebuggerTypeProxy)
  • 详解 .NET 反射中的 BindingFlags 以及常用的 BindingFlags 使用方式
  • 在 csproj 文件中使用系统环境变量的值(示例将 dll 生成到 AppData 目录下)
  • git subtree 的使用
  • 让你的 VSCode 具备调试 C# 语言 .NET Core 程序的能力
  • 手工编辑 tasks.json 和 launch.json,让你的 VSCode 具备调试 .NET Core 程序的能力
  • C#/.NET 如何结束掉一个进程
  • C#/.NET 移动或重命名一个文件夹(如果存在,则合并而不是出现异常报错)
  • 如何创建应用程序清单文件 App.Manifest,如何创建不带清单的应用程序
  • 应用程序清单 Manifest 中各种 UAC 权限级别的含义和效果
  • 启用 Windows 审核模式(Audit Mode),以 Administrator 账户来设置电脑的开箱体验
  • Windows 中的 UAC 用户账户控制
  • Windows 下使用 runas 命令以指定的权限启动一个进程(非管理员、管理员)
  • Windows 的 UAC 设置中的通知等级实际上只有两个档而已
  • JavaScript 是如何工作的:WebRTC 和对等网络的机制!
  • Promise初体验
  • Redis 懒删除(lazy free)简史
  • 阿里中间件开源组件:Sentinel 0.2.0正式发布
  • 关键词挖掘技术哪家强(一)基于node.js技术开发一个关键字查询工具
  • 目录与文件属性:编写ls
  • 使用 Docker 部署 Spring Boot项目
  • 阿里云服务器如何修改远程端口?
  • ​【已解决】npm install​卡主不动的情况
  • # 学号 2017-2018-20172309 《程序设计与数据结构》实验三报告
  • (day6) 319. 灯泡开关
  • (echarts)echarts使用时重新加载数据之前的数据存留在图上的问题
  • (十一)图像的罗伯特梯度锐化
  • * 论文笔记 【Wide Deep Learning for Recommender Systems】
  • .net 逐行读取大文本文件_如何使用 Java 灵活读取 Excel 内容 ?
  • .NET的微型Web框架 Nancy
  • .net连接oracle数据库
  • .NET性能优化(文摘)
  • .Net中间语言BeforeFieldInit
  • .NET中使用Protobuffer 实现序列化和反序列化
  • /proc/stat文件详解(翻译)
  • @font-face 用字体画图标
  • [AIR] NativeExtension在IOS下的开发实例 --- IOS项目的创建 (一)
  • [C++]18:set和map的使用
  • [JavaEE] 线程与进程的区别详解
  • [JavaWeb]—Spring入门
  • [leetcode] 四数之和 M
  • [NOI 2016]优秀的拆分
  • [NOIP2014] 提高组 洛谷P1941 飞扬的小鸟
  • [NOIP2017 提高组] 列队 题解
  • [NSSRound#16 Basic]RCE但是没有完全RCE
  • [PAT] 1041 Be Unique (20 分)Java
  • [PHP]实体类基类和序列化__sleep问题
  • [Python] 递归返回值 为 None 的问题
  • [ruby on rails] ruby使用vscode做开发
  • [Spark、hadoop]spark Streaming的核心DStream
  • [VulnHub靶机渗透] pWnOS 2.0
  • [爱情] 『转载』女生写的追MM秘籍,看了马上告别光棍
  • [笔记].使用Protues仿真Max7129
  • [国嵌攻略][051][NandFlash原理解析]