本节书摘来自华章出版社《Effective Ruby:改善Ruby程序的48条建议》一书中的第1章,第1.4节,作者[美]彼得 J.琼斯(Peter J. Jones),更多章节内容可以访问云栖社区“华章计算机”公众号查看
第4条:留神,常量是可变的
如果你学习Ruby之前学过其他的编程语言,Ruby中常量的行为很可能和你预料的不同。不过,在深挖之前让我们回顾一下在Ruby中什么是常量。
当你初次学习Ruby时,可能有人教你,说常量是由大写字母和下划线组成的标识符。一些例子包括STDIN、ARGV以及RUBY_VERSION。不过这不是故事的全部。事实上,由大写字母开头的任何标识符都是常量。这表示String或Array也都是常量。没错,就是这样……Ruby中那些类和模块的名字事实上都是常量。记着这些,让我们来深入了解在Ruby中常量和其他看似变量的东西有什么区别。
正如其名字建议的那样,常量原打算在程序的生命周期中保持不变。因此,你可能假设Ruby会阻止你改变常量的值的行为。好吧,你的假设是错的。来看看这个:
如果调用方法unreachable时没有加参数的话,就会意外地改变一个常量的值。在Ruby中这样做甚至都不会警告你。本质上讲,Ruby中的常量相比不变值来说更像是全局变量。假如你仔细想想,既然类和模块名都是常量,那么你可以在任何时候改变一个类(比如增加些方法),引用类的那些对象也会随之变化。这些对类和模块来说都没有问题,不过对那些我们真正希望是不变量的常量来说就不那么美好了。还好,有一种解决这个问题的方法——freeze方法:
适当地做以上变化,假如再想改变常量NETWORKS的值时,purge_unreachable方法就会引发RuntimeError异常。根据一般的经验,总是通过冻结常量来阻止其被改变。然而不幸的是,冻结NETWORKS数组还不够,来看看这个:
如果第二个参数没有赋值,那么host_addresses方法会修改数组NETWORKS的元素。即使数组NETWORKS自身被冻结,但其元素仍然是可变的。你可能无法从数组中增删元素,但你一定可以对存在的元素加以修改。因此,如果一个常量引用了一个集合,比如数组或是散列,那么请冻结这个集合以及其中的元素:
(如果你恰巧使用了Ruby 2.1或更高版本,你可以使用第47条中的小技巧直接冻结字符串的字面量。这可以在防止元素被意外改变的同时节省一些内存。)
冻结常量可以把一个隐晦的、难以发现的问题变成一个异常。这显然是很好的。然而,这还不够。即使冻结了一个变量所指向的对象,你仍然会在给存在的常量赋予新值时引发问题。不信请看这个例子:
如你所见,在Ruby中给存在的常量赋予新值是完全合法的。你也看到Ruby产生了警告,说我们正在改变一个常量的值。不过仅此而已。还好,如果我们自己处理这些事情,可以让Ruby在我们意外地重定义常量时引发异常。不过这个解决方案有些笨拙,并且在某些时候显得有些粗暴,但胜在简单。要防止Ruby为常量重新赋值,可以冻结常量所在的类或者模块。你甚至可能会想结构化你的代码,从而将所有常量都定义在它们各自的模块中,从而隔绝freeze方法的影响。
定义常量时,你可以考虑在三个级别进行冻结。前两个比较简单:冻结常量引用的对象以及定义常量的模块。这两步防止了常量被改变和重新赋值。第三种则有些复杂。我们看到,如果常量引用了一个字符串数组,我们就需要冻结这个数组以及其所有元素。换句话说,需要深层冻结该常量引用的这个对象。每个常量都会有所不同,我们只需保证它被完全冻结就可以了。
要点回顾
总是将常量冻结,从而防止其被改变。
如果常量引用了一个集合对象比如数组或散列,那么冻结这个集合及其所有
元素。
要防止常量被重新赋值,可以冻结定义它的那个模块。