3

我知道 Ruby 中的“常量”按照惯例称为常量,但实际上是可变的。但是,我的印象是,当它们“变异”时会发出警告:

class Z2
 M = [0,1]
end

Z2::M # => [0, 1]
Z2::M = [0,3]
(irb):warning: already initialized constant Z2::M
(irb):warning: previous definition of M was here

但是我发现情况并非一直如此:

a = Z2::M
a[1] = 2

Z2::M # => [0,2] and no warning

这是“警告”系统的漏洞吗?我推断一个常量的赋值会重复它,但我猜这也不正确,因为常量和变量似乎指向同一个对象?这是否意味着所有所谓的“常量”都需要被冻结,以防止它们在没有警告的情况下被更改?

4

3 回答 3

6

TL;博士

如果没有猴子修补内核#warn(请参阅https://stackoverflow.com/a/662436/1301972)引发异常,您将无法阻止重新分配给常量本身。在惯用的 Ruby 代码中,这通常不是一个实用的问题,人们希望能够做诸如重新打开类之类的事情,即使类名也是常量。

Ruby 常量实际上不是不可变的,您不能冻结变量。但是,当尝试修改常量引用的冻结对象的内容时,您可能会引发异常。

用纯红宝石深度冻结物体

冻结数组很容易:

CONSTANT_ONE = %w[one two three].freeze

但是存储在这个 Array 中的字符串实际上是对 String 对象的引用。因此,虽然您不能修改此 Array,但您仍然可以(例如)修改索引 0 引用的 String 对象。要解决此问题,您不仅需要冻结 Array,还需要冻结它所包含的对象。例如:

CONSTANT = %w[one two three].map(&:freeze).freeze

CONSTANT[2] << 'four'
# RuntimeError: can't modify frozen String

CONSTANT << 'five'
# RuntimeError: can't modify frozen Array

使用 Gem 递归地冻结对象

由于冻结递归引用可能有点笨拙,很高兴知道有一个宝石可以解决这个问题。您可以使用 ice_nine 来深度冻结大多数对象:

require 'ice_nine'
require 'ice_nine/core_ext/object'

OTHER_CONST = %w[a b c]
OTHER_CONST.deep_freeze

OTHER_CONST << 'd'
# RuntimeError: can't modify frozen Array

OTHER_CONST[2] = 'z'
# RuntimeError: can't modify frozen Array

使用 Ruby 常量的更好方法

另一个要考虑的选项是在将常量的值分配给另一个变量(例如类初始值设定项中的实例变量)时调用Object#dup,以确保您不会意外改变常量的引用。例如:

class Foo
  CONSTANT = 'foo'
  attr_accessor :variable

  def initialize
    @variable = CONSTANT.dup
  end
end

foo = Foo.new
foo.variable << 'bar'
#=> "foobar"

Foo::CONSTANT
#=> "foo"
于 2014-10-24T06:14:26.757 回答
1

There is no gap, as you are not altering a constant. And the fact is that Ruby constants are just variables with extra warnings.

Constant, just as every variable, is merely a pointer to the object in memory. When you doM = [0,3] you are creating a new array and re-pointing constant to this new object, which triggers a warning.

However, when you run M[0] = 1 you are just modifying referenced object, but you do not change the constant, as it still points to the same object.

Important thing to realize here is that all classes in Ruby are just objects in memory, referenced with constants, so when you do:

class Z2
end

it is equivalent to (if Z2 is not defined or is not pointing onto a class object already):

Z2 = Class.new

Naturally class is a very dynamic object, as we keep adding methods to it and so on - we definitively don't want this to trigger any warnings.

于 2014-10-23T21:33:41.167 回答
0

如果你这样做Z2::M[1] = 2,你也不会收到消息。我相信缺少警告是因为您正在更改Array本身而不是参考Z2::M
如果M是整数,例如:

class Z2
  M = 1
end

a = Z2::M
a = 2
a # => 2
Z2::M # => 1

要修改Array常量而不修改原始常量,您可以执行以下操作:

class Z2
  M = [0,1]
end

a = Z2::M.dup
a[0] = 1
a # => [1,1]
Z2::M # => [0,1]
于 2014-10-23T21:39:46.103 回答