Effective-CSharp-2考虑用readonly代替const
C# 有两种常量,一种是**编译期 (compile-time)的,另一种是运行期 (runtime)**的。[1]
1 | // Compile-time constant: |
以上代码展示了如何在 class 或 struct 的范围内声明这两种常量 此外编译期常量还能在方法中声明,而 readyonly 常量则不行。[2]
编译器的常量取值嵌入目标代码中。例如[3]
1 | if(myDateTime.Year == Millennium) |
编译成 IL 之后,与直接使用字面量2000是一样的
1 | if(myDateTime.Year == 2000) |
运行期常量与之不同,如果代码中使用到了运行期常量,那么其生成的 IL 的、也会同样引用该变量,而不会直接使用字面量。[4]
这两种变量支持的值也不一样。编译期的常量只能用来表示内置的 int, float, enum, string。非基本变量不能使用编译期常量声明,需要使用 readonly。在生成 IL 的过程中,只有用来表示这些原始类型的编译期常量才会替换成字面量。[5]
无法编译,试图使用 new 操作符进行初始化: (Ryuu: 即使是参数是值类型也不行,其对象在编译期不存在)
1 | // DON'T DO THIS! |
**编译期常量只能用数字,字符串或 null 初始化。**readonly 常量在执行完构造函数 (constructor) 之后不可以再修改。(和编译期常量不同,他的值是在执行完构造函数后才初始化的)[6]
在生成 IL 的时候,代码中的编译期常量会直接以其常量值写入,如果在制作另外的程序集 (assembly) 的时候用到了该程序集中的编译期常量,那么这个常量将会以字面值写入另外的程序集。[7]
有的时候开发者确实想把某个值固定在编译期,比如程序版本记录,如果更新整个项目,那么里面每个版本号都会变为最新,如果仅更新其中某些程序集,那么只有更新的程序集的版本号会变为最新值。
const 的性能比 readonly 的要好。由于程序集可以直接访问值,而不用查询变量,因此性能稍高。但是,开发者需要考虑是否值得为了这点性能而使得代码变得僵硬。在决定这么做之前,您应该先通过 profile 工具做性能测试。(可以试试 BenchmarkDotNet)[8]
const 关键字用来声明那些必须在编译期得以确定的值,例如 attribute 参数、switch case 语句的标签、enum 的定义等,偶尔用于声明不会随版本而变化的值。除此之外的值考虑用 readonly 常量声明。[9]
Bill Wagner 的 Effective C# (Covers C# 6.0), 3rd Edition Item 2 将
const与readonly的选择放在“编译期常量 vs. 运行期字段”的语义边界下讨论;Microsoft 的const与readonly文档也分别把二者定义为编译期常量和只读字段约束:https://www.informit.com/store/effective-c-sharp-covers-c-sharp-6.0-50-specific-ways-9780134579283、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/const、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly。 ↩︎Microsoft
const文档说明常量可声明为局部常量或字段;readonly文档则把readonly描述为字段修饰符,赋值位置受字段初始化器和构造函数约束:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/const、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly。 ↩︎C# 规范 §15.5.3.3 在讨论 constants 与 static readonly fields 的版本差异时说明,常量的值会在编译期取得;这就是跨程序集使用
const时会出现字面值写入调用方编译产物的根源:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#15533-versioning-of-constants-and-static-readonly-fields。 ↩︎同一段 C# 规范把
static readonly字段与const对比:字段值在运行期获得,而不是像常量那样在编译期写入使用方:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#15533-versioning-of-constants-and-static-readonly-fields。 ↩︎Microsoft
const文档列出的可用类型比正文示例更完整:内置数值类型、bool、char、string、枚举类型,以及引用类型的null。这里的关键边界不是这几个例子本身,而是初始化表达式必须能在编译期成为常量表达式:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/const、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1226-constant-expressions。 ↩︎C# 规范 §15.5.3 规定
readonly字段只能在声明的变量初始化器、实例构造函数、静态构造函数或静态字段初始化器中赋值;但readonly不是深度不可变,引用对象内部是否可变仍取决于对象自身设计:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#1553-readonly-fields、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly。 ↩︎C# 规范 §15.5.3.3 专门用 constants 与 static readonly fields 对比说明 binary versioning:常量在编译期取值,静态只读字段在运行期取值;因此公开
const的变更通常要求依赖方重新编译才能看到新值:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#15533-versioning-of-constants-and-static-readonly-fields。 ↩︎Microsoft CA1802 “Use literals where appropriate” 将
static readonly改为const视为可能的性能优化,但规则说明也把适用范围限定在值可在编译期确定且语义上适合常量的场景;公开 API 还要先考虑版本语义:https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1802、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#15533-versioning-of-constants-and-static-readonly-fields。 ↩︎attribute 参数类型受 C# 规范 §23.2.4 限制,常见 attribute 参数、
case标签和枚举值都属于需要编译期常量表达式的语境;这些场景才是const的天然职责:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/attributes#2324-attribute-parameter-types、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1226-constant-expressions。 ↩︎