静态只读 vs 常量 — 不同程序集的观点?

20

有很多关于这个主题的问题,但除了 一个非常简短的问题之外,没有一个问题涉及以下情况。

来自C# 4书:

enter image description here

Marc 也写道:

如果您更改常量的值,则需要重新构建所有客户端。

问题:

1)为什么? static readonlyconst 都是 static 的吗?

2)实际上,值保存在哪里?

3)使字段 static readonly 究竟是如何 "在幕后" 解决这个问题的?


2
这是因为编译器将内联常量的值,而不是引用来自另一个程序集的变量。 - ken2k
2
从未了解过常量在程序集之间的这种行为。好问题。 - Oleg Grishko
5个回答

26
如果更改常量的值,您需要重新构建所有客户端。 这并不是正确的解决方案。如果更改常量的值,则它就不是常量。常量根据定义是永远不会改变其值的东西。认为你会改变一个常量的值意味着你正在做一些逻辑上不可能的事情,所以当然会出现问题;你正在做你说过你不会做的事情。如果你向编译器撒谎,并且在这样做时感到疼痛,那么请停止向编译器撒谎。
黄金价格不是常数。你银行的名字也不是常数。你程序的版本号也不是常数。这些东西会改变,因此不要将它们设置为常数。常数是像圆周率或一个金原子中的质子数这样的东西。
变量是可以变化的东西——这就是为什么它们被称为“变量”的原因。常数是保持不变的东西。如果它能变化,就把它变成变量。如果它是恒定的,请将其设置为常数。就这么简单。
C#中的“static”表示“命名元素与类型关联,而不是与类型的任何特定实例相关联”。(因此,“静态”是一个不好的术语选择;VB用“共享”做得更好。)名称是与类型还是实例相关联,与名称是否指代常量或变量的问题无关。
在静态只读和常量中,值实际上保存在哪里?当您使用常量值时,该值在使用的任何地方都是“内置的”。这是安全的,因为它永远不会改变。它永远不会改变,因为它是常量,这就是“常量”的含义。
当您使用变量时,每次运行时都会查找变量的值。“readonly”只是意味着“此变量只能在类构造函数或字段初始化器中更改”。它仍然是一个变量。
如何使字段成为静态只读 - 实际上是解决这个问题的方法?
由于您没有说明问题所在,因此我不知道您正在尝试解决什么问题。
只读字段被认为是非常数值,在构造函数之外,因此不可变突变值类型的只读字段不能突变,因此您无法获取对只读字段的引用,然后突变引用。

1
现在我明白这句话的意思了!:如果你欺骗编译器,它会报复的--亨利·斯宾塞 - Ayub
你的写作非常好。为什么不写一本关于C#的伟大书籍呢?这是我的梦想! - Ayub
1
@程序员1:谢谢!我在写《Essential C#》的最后几个版本时帮助了马克,但大部分写作是他完成的。我曾考虑过写一本自己的C#书,但这需要很多工作。此外,我还写了一篇关于C#设计的博客。 - Eric Lippert
我对这个答案的问题在于它假设了“更改常量的值”的含义,我认为这与@marc Gravell所想的相矛盾。我理解他的意思是“更改const XYZ的值意味着您更改了程序集A中const XYZ的值,该程序集被程序集B引用。当构建程序集B时,程序集A中XYZ的值被嵌入到程序集B中。如果您随后更改程序集A中XYZ的值并重新构建它,则程序集B仍然看到旧值。” - David A. Gray

25

不,const就是const,不是static。它是一种特殊情况,有着不同的规则;它仅在编译时设置(而非运行时),并且处理方式也不同。

关键在于以下内容的含义:

var foo = SomeType.StaticValue;

对比

var bar = SomeType.ConstValue;

在第一种情况下,它从SomeType中在运行时读取值,即通过ldsfld; 然而,在第二种情况下,它被编译成该值,也就是说,如果ConstValue恰好等于123,那么第二种情况与以下内容相同

var bar = 123;

在运行时,由于值123已经被编译器评估并存储,因此它来自SomeType的事实是不存在的。因此需要重新构建以获取新值。

将其更改为static readonly意味着“从SomeType加载值”的操作得以保留。

所以以下代码:

static int Foo()
{
    return Test.Foo;
}
static int Bar()
{
    return Test.Bar;
}
...
static class Test
{
    public static readonly int Foo = 123;
    public const int Bar = 456;
}

编译后的结果如下:

.method private hidebysig static int32 Bar() cil managed
{
    .maxstack 8
    L_0000: ldc.i4 0x1c8
    L_0005: ret 
}

.method private hidebysig static int32 Foo() cil managed
{
    .maxstack 8
    L_0000: ldsfld int32 ConsoleApplication2.Test::Foo
    L_0005: ret 
}
请注意,在Bar中,ldc是直接加载一个值(0x1c8 == 456),完全没有使用Test
为了完整起见,const是通过静态字段实现的,但它是一个字面量字段,这意味着它是在编译时而不是运行时计算的。
.field public static literal int32 Bar = int32(0x1c8)
.field public static initonly int32 Foo

感谢您的出色回答,但是const不是隐式静态的吗? - Royi Namir
1
@Royi 嗯,从实现上来说是的,因为它在IL中以 .field ... static 的形式实现,在C#中则通过 TypeName.ConstName 访问。但重要的是语义;就语义而言,访问常量成员与访问常规静态成员(字段/属性)之间有很大的区别。 - Marc Gravell

6

1) const在编译时使用您提供的值进行解析。而static readonly是一个静态变量。

2) static值通常存储在堆上的特殊区域中,称为高频堆。正如我之前所说,常量在编译时被替换。

3) 将其设置为static readonly将解决问题,因为您将在运行时读取变量值,而不是编译时提供的值。


4
您已经通过链接的图像回答了您的问题。 const字段将被编译(“内联”)到汇编中-就像简单的搜索和替换一样。 static readonly表示一个普通字段,不允许更改,并且仅存在于内存中一次,但仍然由内存位置引用。
在.NET Framework中,常量不会分配内存区域,而是被视为值。因此,您永远无法分配常量,但将常量加载到内存中更有效,因为它可以直接注入指令流中。这消除了任何内存之外的内存访问,提高了参考局部性。 http://www.dotnetperls.com/optimization

1
我想我们可以将常量视为代码中的硬编码值,但提供更好的维护性和可用性。

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