常数值可以随时间改变吗?


28

在开发阶段,某些变量需要在同一运行中进行固定,但可能需要随时间进行修改。例如boolean,使用信号通知调试模式,因此我们可以在程序中执行通常不会执行的操作。

在常量中(例如,final static int CONSTANT = 0在Java中)包含这些值是否不好?我知道常量在运行时保持不变,但是在整个开发过程中,除了计划外的更改之外,常量也应该保持不变吗?

我搜索了类似的问题,但没有找到与我完全匹配的任何内容。


11
我很好奇,为什么您会认为更改这些样式是不好的呢?
文森特·萨瓦德

36
除非您使用具有已知数学值的常数对物理属性进行建模,否则所有内容都会在某个时间发生变化。
Berin Loritsch '18

19
软件是软的
埃里克·艾德

10
@GregT我不同意。final为您提供了由编译器强制执行的保证,即程序不会修改该值。我不会因为程序员可能想修改源代码中分配的值而放弃它。
亚历山大-恢复莫妮卡

10
没有足够的时间来表达一个完整的答案,但是我怀疑您的同事关注的不是常量,而是配置值的代码内联,配置值可能会表现为常量。...常量可以保护您避免愚蠢的错误,例如在gravity游戏中期/运行中意外分配。他们不一定意味着gravity在每个星球上都一样。也就是说,健康的解决方案是使gravity一个常数不变,但planet要在相关范围开始时从文件或数据库中提取它。
svidgen

Answers:


6

在Java中,编译器可以将静态最终常量作为其值复制到使用它们的代码中。结果,如果您发布代码的新版本,并且下游常量使用了该常量,则除非重新编译下游代码,否则该代码中的常量将不会更新。如果他们随后将常量与需要新值的代码一起使用,则可能会出现问题,即使源代码正确,而二进制代码也不正确。

这是Java设计中的一个缺陷,因为它是极少数情况(可能是唯一的情况)中源兼容性和二进制兼容性不同的情况之一。除这种情况外,您可以使用新的API兼容版本替换依赖项,而无需依赖项的用户重新编译。显然,鉴于通常管理Java依赖项的方式,这非常重要。

更糟糕的是,代码只会默默地做错事而不是产生有用的错误。如果要用具有不兼容的类或方法定义的版本替换依赖项,则会出现类加载器或调用错误,这至少可以提供有关问题所在的良好线索。除非您更改了值的类型,否则此问题将显示为神秘的运行时行为。

更令人烦恼的是,当今的JVM可以轻松地在运行时内联所有常量而不会降低性能(除了需要加载定义常量的类(可能一直在加载)之外,不幸的是,语言的语义可以追溯到JIT之前的日子。 。而且他们无法更改语言,因为使用先前的编译器编译的代码将不正确。兼容漏洞再次出现。

因此,有人建议完全不要更改静态最终值。对于可能在未知时间广泛分发并以未知方式更新的库,这是一个好习惯。

在您自己的代码中,尤其是在依赖关系层次结构的顶部,您可能会摆脱它。但是在这种情况下,请考虑是否真的需要将该常量公开(或受保护)。如果该常量仅是程序包可见性,则根据您的情况和代码标准,合理的做法是始终总是立即重新编译整个程序包,然后问题就消失了。如果常量是私有的,则没有问题,可以随时更改它。


85

源代码中的任何内容(包括const声明的全局常数)都可能会随软件的新发行版而更改。

关键字const(或finalJava中的关键字)用于向编译器发出信号,表示在程序的此实例运行时此变量不会更改。而已。如果要将消息发送给下一个维护者,请在源中使用注释,这就是它们的作用。

// DO NOT CHANGE without consulting with the legal department!
// Get written consent form from them before release!
public const int LegalLimitInSeconds = ...

是与自己未来沟通的更好方法。


11
我真的很喜欢这个评论对未来的自我。
GregT '18年

4
TaxRatepublic让我紧张。我想确定的是,只有销售部门会受到此更改的影响,我们的供应商也不会受到此影响。自编写该注释以来,谁知道代码库中发生了什么。
candied_orange

3
@IllusiveBrian没有批评常量的使用。警告不要信任评论是最新的。更改之前,请务必确定其用法。
candied_orange

8
这是Java的好建议。其他语言可能有所不同。例如,由于将const值绑定到C#中的调用站点的方式,因此public const字段只能用于永远不变的事物,例如Math.pi。如果要创建库,则在开发过程中或使用新版本时可能会发生变化的应该是public static readonly,以免引起库用户的问题。
GrandOpener

6
您应该选择一个不同的示例...金钱值永远不应是浮点数!
corsiKa

13

我们需要区分常量的两个方面:

  • 为开发时已知的值命名,为更好的可维护性引入了这些值,以及
  • 编译器可用的值。

还有第三种相关的变量:其值不变的变量,即值的名称。这些不可变变量和常量之间的区别是,在确定/分配/初始化值时:在运行时初始化变量,但是在开发过程中知道常量的值。这种区别有点混乱,因为在开发过程中可能知道一个值,但实际上仅在初始化期间创建了一个值。

但是,如果常量的值在编译时已知,则编译器可以使用该值执行计算。例如,Java语言具有常量表达式的概念。常量表达式是仅由基元或字符串的文字,对常量表达式的操作(例如强制转换,加法,字符串连接)和常量变量组成的任何表达式。[ JLS§15.28 ]常量变量是final使用常量表达式初始化的变量。[JLS§4.12.4]因此对于Java,这是一个编译时常量:

public static final int X = 7;

当在多个编译单元中使用常量变量,然后更改声明时,这变得很有趣。考虑:

  • A.java

    public class A { public static final int X = 7; }
  • B.java

    public class B { public static final int Y = A.X + 2; }

现在,当我们编译这些文件时,B.class字节码将声明一个字段,Y = 9因为它B.Y是一个常量变量。

但是,当我们将A.X变量更改为其他值(例如X = 0)并仅重新编译A.java文件时,B.Y仍然会引用旧值。此状态A.X = 0, B.Y = 9与源代码中的声明不一致。调试愉快!

这并不意味着不应该更改常量。常量绝对比在源代码中没有任何解释的幻数更好。但是,公共常量是公共API的一部分。这不是特定于Java,而是在C ++和其他具有单独编译单元的语言中发生。如果更改这些值,则需要重新编译所有相关代码,即执行干净编译。

根据常量的性质,它们可能导致开发人员的错误假设。如果更改了这些值,则可能会触发错误。例如,可以选择一组常数,以便它们形成某些位模式,例如public static final int R = 4, W = 2, X = 1。如果将这些更改为类似的结构,R = 0, W = 1, X = 2则现有的代码(例如)将boolean canRead = perms & R变得不正确。只是想想随之而来的乐趣就是Integer.MAX_VALUE改变!这里没有解决方法,重要的是要记住一些常量的值确实很重要,不能简单地更改。

但是对于大多数常量而言,只要考虑上述限制,就可以更改它们。当含义而不是特定值很重要时,可以安全地更改常量。例如,诸如BORDER_WIDTH = 2TIMEOUT = 60; // seconds或之类的模板就是这种情况,API_ENDPOINT = "https://api.example.com/v2/"尽管可以说其中的一些或全部应该在配置文件中指定,而不是在代码中指定。


5
我喜欢这种分析。我读为:只要您了解常数的使用方式,就可以自由更改常数。
candied_orange

+1 C#也因公共常量而遭受同样的困扰。
Reginald Blue

6

在应用程序运行时的生命周期内保证常量是恒定。只要这是真的,就没有理由不利用语言功能。您只需要知道出于相同目的而使用常量vs.编译器标志的后果:

  • 常量占用应用程序空间
  • 编译器标志
  • 常量关闭的代码可以使用现代重构工具进行更新和更改
  • 无法通过编译器标志关闭的代码

维护了一个将打开形状的边界框以便可以调试它们的绘制方式的应用程序之后,我们遇到了一个问题。重构后,所有被编译器标志关闭的代码将无法编译。之后,我们有意将编译器标志更改为应用程序常数。

我的意思是证明存在权衡。少量的布尔值不会使应用程序耗尽内存或占用太多空间。如果您的常量确实是一个大对象,而该对象本质上可以处理代码中的所有内容,则可能不正确。如果在不再需要某个对象时不小心删除它持有的所有引用,则该对象可能是内存泄漏的根源。

您需要评估用例以及为什么要更改常量。

我不喜欢简单的笼统声明,但总的来说,您的资深同事是正确的。如果某些内容经常更改,那么它可能需要是可配置的项目。例如,IsInDebugMode = true当您想保护某些代码不被破坏时,您可以说服您的同事使用一个常量。但是,某些事情可能需要比发布应用程序更频繁地更改。在这种情况下,您需要一种在适当时间更改该值的方法。您可以以的示例TaxRate = .065。在编译代码时可能是正确的,但是由于新的法律,在发布应用程序的下一版本之前,它可能会更改。需要从某种存储机制(例如文件或数据库)中进行更新


您所说的“编译器标志”是什么意思?也许支持宏/定义和#ifdefs 的C预处理器和类似的编译器功能?由于这些是基于源代码的文本替换,因此它们不是编程语言语义的一部分。请注意,Java没有预处理器。
阿蒙(Amon)

@ amon,Java可能没有,但是几种语言可以。我的意思是#ifdef旗帜。尽管它们不是C语义的一部分,但它们是C#的一部分。我在为语言不可知论的更广泛的上下文而写作。
Berin Loritsch '18

我认为“内存浪费”论点尚无定论。内联和摇树是任何发布模式优化器中几乎通用的步骤。
亚历山大-恢复莫妮卡

@亚历山大,我同意。这是需要注意的事情。
Berin Loritsch

1
“常量占用了应用程序空间”-除非您正在为只有一千个或两个字节内存的微控制器开发嵌入式应用程序,否则您甚至都不要考虑这些事情。
vsz

2

const#define或者final是一个编译器的提示(注意,#define实际上不是一个暗示,它是一个宏观和显著更强大)。它表示该值在程序执行过程中不会改变,可以进行各种优化。

但是,作为编译器提示,编译器会执行程序员可能并不总是期望的事情。特别是,javac将内联a,static final int FOO = 42;以便在任何FOO使用位置,实际的已编译字节代码都将读取42

除非有人在不重新编译另一个编译单元(.java文件)的情况下更改了值,然后42保留字节码中的剩余部分(请参见是否有可能禁用javac的静态最终变量内联?),这并不令人感到意外。

做出一些事情static final意味着它将会是永恒的,改变它的确是一件大事-尤其是如果它什么都不做private

诸如此类的常数final static int ZERO = 0不是问题。 final static double TAX_RATE = 0.55(除了赚钱和加倍是不好的,应该使用BigDecimal,但它不是原始的,因此不能内联)是一个问题,应仔细检查其使用位置。


零值。

3
is a problem and should be examined with great care for where it is used.为什么会出问题呢?
亚历山大-恢复莫妮卡

1

顾名思义,常量在运行时不应更改并且我认为常量被定义为长期不更改(您可以查看此SO问题以获取更多信息。

如果需要标志(例如,用于开发模式),则应改用配置文件或启动参数(许多IDE支持在每个项目的基础上配置启动参数;请参阅相关文档)以启用此模式-通过这种方式,您可以灵活地使用这种模式,并且每次执行代码时都不能忘记对其进行更改。


0

能够在运行之间进行更改是在源代码中定义常量的最重要要点之一!

该常量为您提供了一个定义明确且有据可查的位置(从某种意义上来说),可以在源代码有效期内随时更改此值。还可以保证,更改此位置处的常数实际上将更改其所代表的所有出现。

作为一个负面例子:它会不会是有意义的具有恒定的TRUE,其演算值为true其中居然有一个语言true关键字。TRUE=false除了一个残酷的笑话,你永远也不会,甚至不会一次宣布。

当然,还有其他常量的用法,例如缩短代码(CO_NAME = 'My Great World Unique ACME Company'),避免重复(PI=3.141),设置约定(TRUE=1)或其他任何方法,但是具有更改常量的定义位置无疑是最突出的一种。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.